Merge branch '3.4'
* 3.4: [DI] prepare for signature change in 4.0 [DI] Add missing deprecation on Extension::getClassesToCompile [Lock] Re-add the Lock component in 3.4 [Routing] remove an unused routing fixture [Yaml] fix multiline block handling [WebProfilerBundle] Fix sub-requests display in time profiler panel [FrameworkBundle] Handle project dir in cache:clear command [WebServerBundle] Mark ServerCommand as internal [DI] Fix autowire error for inlined services Close PHP code in phpt file [Profiler][VarDumper] Fix searchbar css when in toolbar Prevent auto-registration of UserPasswordEncoderCommand [Process] Fixed incorrectly escaping arguments on Windows when inheritEnvironmentVariables is set to false avoid double blanks while rendering form attributes use getProjectDir() when possible [PhpUnitBridge] add a changelog file [FrameworkBundle][Validator] Deprecate passing validator instances/aliases over using the service locator
This commit is contained in:
commit
3574455c59
|
@ -244,6 +244,10 @@ FrameworkBundle
|
|||
class has been deprecated and will be removed in 4.0. Use the
|
||||
`Symfony\Component\Workflow\DependencyInjection\ValidateWorkflowsPass` class instead.
|
||||
|
||||
* Passing an array of validators or validator aliases as the second argument of
|
||||
`ConstraintValidatorFactory::__construct()` is deprecated since 3.3 and will
|
||||
be removed in 4.0. Use the service locator instead.
|
||||
|
||||
HttpFoundation
|
||||
--------------
|
||||
|
||||
|
@ -262,7 +266,7 @@ HttpKernel
|
|||
* Deprecated the `Kernel::getRootDir()` method. Use the new `Kernel::getProjectDir()`
|
||||
method instead.
|
||||
|
||||
* The `Extension::addClassesToCompile()` method has been deprecated and will be removed in 4.0.
|
||||
* The `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()` methods have been deprecated and will be removed in 4.0.
|
||||
|
||||
* The `Psr6CacheClearer::addPool()` method has been deprecated. Pass an array
|
||||
of pools indexed by name to the constructor instead.
|
||||
|
|
|
@ -301,6 +301,10 @@ FrameworkBundle
|
|||
|
||||
* Extending `ConstraintValidatorFactory` is not supported anymore.
|
||||
|
||||
* Passing an array of validators or validator aliases as the second argument of
|
||||
`ConstraintValidatorFactory::__construct()` has been removed.
|
||||
Use the service locator instead.
|
||||
|
||||
* Class parameters related to routing have been removed
|
||||
* router.options.generator_class
|
||||
* router.options.generator_base_class
|
||||
|
@ -374,7 +378,7 @@ HttpKernel
|
|||
* Removed the `Kernel::getRootDir()` method. Use the `Kernel::getProjectDir()`
|
||||
method instead.
|
||||
|
||||
* The `Extension::addClassesToCompile()` method has been removed.
|
||||
* The `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()` methods have been removed.
|
||||
|
||||
* Possibility to pass non-scalar values as URI attributes to the ESI and SSI
|
||||
renderers has been removed. The inline fragment renderer should be used with
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"symfony/inflector": "self.version",
|
||||
"symfony/intl": "self.version",
|
||||
"symfony/ldap": "self.version",
|
||||
"symfony/lock": "self.version",
|
||||
"symfony/monolog-bridge": "self.version",
|
||||
"symfony/options-resolver": "self.version",
|
||||
"symfony/process": "self.version",
|
||||
|
|
|
@ -19,3 +19,9 @@ CHANGELOG
|
|||
deprecated, use the `@group legacy` notation instead
|
||||
* using the `Legacy` prefix in class names to mark a test as legacy is deprecated,
|
||||
use the `@group legacy` notation instead
|
||||
|
||||
3.1.0
|
||||
-----
|
||||
|
||||
* passing a numerically indexed array to the constructor of the `SymfonyTestsListenerTrait`
|
||||
is deprecated, pass an array of namespaces indexed by the mocked feature instead
|
||||
|
|
|
@ -17,6 +17,8 @@ require PHPUNIT_COMPOSER_INSTALL;
|
|||
require_once __DIR__.'/../../bootstrap.php';
|
||||
require __DIR__.'/fake_vendor/autoload.php';
|
||||
require __DIR__.'/fake_vendor/acme/lib/deprecation_riddled.php';
|
||||
|
||||
?>
|
||||
--EXPECTF--
|
||||
Unsilenced deprecation notices (2)
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
{{- block('choice_widget_options') -}}
|
||||
</optgroup>
|
||||
{%- else -%}
|
||||
<option value="{{ choice.value }}"{% if choice.attr %} {% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
|
||||
<option value="{{ choice.value }}"{% if choice.attr %}{% with { attr: choice.attr } %}{{ block('attributes') }}{% endwith %}{% endif %}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is same as(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
{%- endblock choice_widget_options -%}
|
||||
|
@ -259,7 +259,7 @@
|
|||
{% set label = name|humanize %}
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
<label{% if label_attr %} {% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>{{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }}</label>
|
||||
<label{% if label_attr %}{% with { attr: label_attr } %}{{ block('attributes') }}{% endwith %}{% endif %}>{{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }}</label>
|
||||
{%- endif -%}
|
||||
{%- endblock form_label -%}
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ CHANGELOG
|
|||
`Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass` instead
|
||||
* Deprecated `ValidateWorkflowsPass`, use
|
||||
`Symfony\Component\Workflow\DependencyInjection\ValidateWorkflowsPass` instead
|
||||
* Deprecated `ConstraintValidatorFactory::__construct()` second argument.
|
||||
|
||||
3.2.0
|
||||
-----
|
||||
|
|
|
@ -46,7 +46,6 @@ class AboutCommand extends ContainerAwareCommand
|
|||
|
||||
/** @var $kernel KernelInterface */
|
||||
$kernel = $this->getContainer()->get('kernel');
|
||||
$baseDir = realpath($kernel->getRootDir().DIRECTORY_SEPARATOR.'..');
|
||||
|
||||
$io->table(array(), array(
|
||||
array('<info>Symfony</>'),
|
||||
|
@ -62,9 +61,9 @@ class AboutCommand extends ContainerAwareCommand
|
|||
array('Environment', $kernel->getEnvironment()),
|
||||
array('Debug', $kernel->isDebug() ? 'true' : 'false'),
|
||||
array('Charset', $kernel->getCharset()),
|
||||
array('Root directory', self::formatPath($kernel->getRootDir(), $baseDir)),
|
||||
array('Cache directory', self::formatPath($kernel->getCacheDir(), $baseDir).' (<comment>'.self::formatFileSize($kernel->getCacheDir()).'</>)'),
|
||||
array('Log directory', self::formatPath($kernel->getLogDir(), $baseDir).' (<comment>'.self::formatFileSize($kernel->getLogDir()).'</>)'),
|
||||
array('Root directory', self::formatPath($kernel->getRootDir(), $kernel->getProjectDir())),
|
||||
array('Cache directory', self::formatPath($kernel->getCacheDir(), $kernel->getProjectDir()).' (<comment>'.self::formatFileSize($kernel->getCacheDir()).'</>)'),
|
||||
array('Log directory', self::formatPath($kernel->getLogDir(), $kernel->getProjectDir()).' (<comment>'.self::formatFileSize($kernel->getLogDir()).'</>)'),
|
||||
new TableSeparator(),
|
||||
array('<info>PHP</>'),
|
||||
new TableSeparator(),
|
||||
|
|
|
@ -82,9 +82,7 @@ EOT
|
|||
$targetArg = rtrim($input->getArgument('target'), '/');
|
||||
|
||||
if (!is_dir($targetArg)) {
|
||||
$appRoot = $this->getContainer()->getParameter('kernel.root_dir').'/..';
|
||||
|
||||
$targetArg = $appRoot.'/'.$targetArg;
|
||||
$targetArg = $this->getContainer()->getParameter('kernel.project_dir').'/'.$targetArg;
|
||||
|
||||
if (!is_dir($targetArg)) {
|
||||
throw new \InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $input->getArgument('target')));
|
||||
|
|
|
@ -14,7 +14,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Validator;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Constraints\Blank as BlankConstraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
class ConstraintValidatorFactoryTest extends TestCase
|
||||
{
|
||||
|
@ -41,6 +44,38 @@ class ConstraintValidatorFactoryTest extends TestCase
|
|||
}
|
||||
|
||||
public function testGetInstanceReturnsService()
|
||||
{
|
||||
$service = 'validator_constraint_service';
|
||||
$validator = $this->getMockForAbstractClass(ConstraintValidator::class);
|
||||
|
||||
// mock ContainerBuilder b/c it implements TaggedContainerInterface
|
||||
$container = $this->getMockBuilder(ContainerBuilder::class)->setMethods(array('get', 'has'))->getMock();
|
||||
$container
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with($service)
|
||||
->willReturn($validator);
|
||||
$container
|
||||
->expects($this->once())
|
||||
->method('has')
|
||||
->with($service)
|
||||
->willReturn(true);
|
||||
|
||||
$constraint = $this->getMockBuilder(Constraint::class)->getMock();
|
||||
$constraint
|
||||
->expects($this->once())
|
||||
->method('validatedBy')
|
||||
->will($this->returnValue($service));
|
||||
|
||||
$factory = new ConstraintValidatorFactory($container);
|
||||
$this->assertSame($validator, $factory->getInstance($constraint));
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
* @expectedDeprecation Passing an array of validators or validator aliases as the second argument of "Symfony\Bundle\FrameworkBundle\Validator\ConstraintValidatorFactory::__construct" is deprecated since 3.3 and will be removed in 4.0. Use the service locator instead.
|
||||
*/
|
||||
public function testGetInstanceReturnsServiceWithAlias()
|
||||
{
|
||||
$service = 'validator_constraint_service';
|
||||
$alias = 'validator_constraint_alias';
|
||||
|
|
|
@ -45,15 +45,16 @@ class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface
|
|||
private $container;
|
||||
private $validators;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param ContainerInterface $container The service container
|
||||
* @param array $validators An array of validators
|
||||
*/
|
||||
public function __construct(ContainerInterface $container, array $validators = array())
|
||||
public function __construct(ContainerInterface $container, array $validators = null)
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
if (null !== $validators) {
|
||||
@trigger_error(sprintf('Passing an array of validators or validator aliases as the second argument of "%s" is deprecated since 3.3 and will be removed in 4.0. Use the service locator instead.', __METHOD__), E_USER_DEPRECATED);
|
||||
} else {
|
||||
$validators = array();
|
||||
}
|
||||
|
||||
$this->validators = $validators;
|
||||
}
|
||||
|
||||
|
@ -82,6 +83,7 @@ class ConstraintValidatorFactory implements ConstraintValidatorFactoryInterface
|
|||
$this->validators[$name] = new $name();
|
||||
}
|
||||
} elseif (is_string($this->validators[$name])) {
|
||||
// To be removed in 4.0
|
||||
$this->validators[$name] = $this->container->get($this->validators[$name]);
|
||||
}
|
||||
|
||||
|
|
|
@ -98,11 +98,11 @@ class SecurityExtension extends Extension
|
|||
|
||||
if ($config['encoders']) {
|
||||
$this->createEncoders($config['encoders'], $container);
|
||||
}
|
||||
|
||||
if (class_exists(Application::class)) {
|
||||
$loader->load('console.xml');
|
||||
$container->getDefinition('security.console.user_password_encoder_command')->replaceArgument(1, array_keys($config['encoders']));
|
||||
}
|
||||
if (class_exists(Application::class)) {
|
||||
$loader->load('console.xml');
|
||||
$container->getDefinition('security.console.user_password_encoder_command')->replaceArgument(1, array_keys($config['encoders']));
|
||||
}
|
||||
|
||||
// load ACL
|
||||
|
|
|
@ -345,6 +345,11 @@ abstract class CompleteConfigurationTest extends TestCase
|
|||
$this->assertEquals('security.user_checker', $this->getContainer('container1')->getAlias('security.user_checker.secure'));
|
||||
}
|
||||
|
||||
public function testUserPasswordEncoderCommandIsRegistered()
|
||||
{
|
||||
$this->assertTrue($this->getContainer('remember_me_options')->has('security.console.user_password_encoder_command'));
|
||||
}
|
||||
|
||||
protected function getContainer($file)
|
||||
{
|
||||
$file = $file.'.'.$this->getFileExtension();
|
||||
|
|
|
@ -107,8 +107,8 @@
|
|||
</form>
|
||||
|
||||
{% if profile.parent %}
|
||||
<h3>
|
||||
Sub-Request {{ profile.getcollector('request').requestattributes.get('_controller') }}
|
||||
<h3 class="dump-inline">
|
||||
Sub-Request {{ profiler_dump(profile.getcollector('request').requestattributes.get('_controller')) }}
|
||||
<small>
|
||||
{{ collector.events.__section__.duration }} ms
|
||||
<a class="newline" href="{{ path('_profiler', { token: profile.parent.token, panel: 'time' }) }}">Return to parent request</a>
|
||||
|
|
|
@ -382,6 +382,9 @@
|
|||
.sf-toolbar-block-dump pre.sf-dump:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.sf-toolbar-block-dump pre.sf-dump .sf-dump-search-wrapper {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.sf-toolbar-block-dump pre.sf-dump span.sf-dump-search-count {
|
||||
color: #333;
|
||||
font-size: 12px;
|
||||
|
|
|
@ -17,6 +17,8 @@ use Symfony\Component\Console\Command\Command;
|
|||
* Base methods for commands related to a local web server.
|
||||
*
|
||||
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class ServerCommand extends Command
|
||||
{
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
<defaults public="false" />
|
||||
|
||||
<service id="web_server.command.server_run" class="Symfony\Bundle\WebServerBundle\Command\ServerRunCommand">
|
||||
<argument>%kernel.root_dir%/../web</argument>
|
||||
<argument>%kernel.project_dir%/web</argument>
|
||||
<argument>%kernel.environment%</argument>
|
||||
<tag name="console.command" />
|
||||
</service>
|
||||
|
||||
<service id="web_server.command.server_start" class="Symfony\Bundle\WebServerBundle\Command\ServerStartCommand">
|
||||
<argument>%kernel.root_dir%/../web</argument>
|
||||
<argument>%kernel.project_dir%/web</argument>
|
||||
<argument>%kernel.environment%</argument>
|
||||
<tag name="console.command" />
|
||||
</service>
|
||||
|
|
|
@ -21,16 +21,30 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|||
class AutowireExceptionPass implements CompilerPassInterface
|
||||
{
|
||||
private $autowirePass;
|
||||
private $inlineServicePass;
|
||||
|
||||
public function __construct(AutowirePass $autowirePass)
|
||||
public function __construct(AutowirePass $autowirePass, InlineServiceDefinitionsPass $inlineServicePass)
|
||||
{
|
||||
$this->autowirePass = $autowirePass;
|
||||
$this->inlineServicePass = $inlineServicePass;
|
||||
}
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
foreach ($this->autowirePass->getAutowiringExceptions() as $exception) {
|
||||
if ($container->hasDefinition($exception->getServiceId())) {
|
||||
// the pass should only be run once
|
||||
if (null === $this->autowirePass || null === $this->inlineServicePass) {
|
||||
return;
|
||||
}
|
||||
|
||||
$inlinedIds = $this->inlineServicePass->getInlinedServiceIds();
|
||||
$exceptions = $this->autowirePass->getAutowiringExceptions();
|
||||
|
||||
// free up references
|
||||
$this->autowirePass = null;
|
||||
$this->inlineServicePass = null;
|
||||
|
||||
foreach ($exceptions as $exception) {
|
||||
if ($container->hasDefinition($exception->getServiceId()) || in_array($exception->getServiceId(), $inlinedIds)) {
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
|||
class InlineServiceDefinitionsPass extends AbstractRecursivePass implements RepeatablePassInterface
|
||||
{
|
||||
private $repeatedPass;
|
||||
private $inlinedServiceIds = array();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
@ -32,6 +33,16 @@ class InlineServiceDefinitionsPass extends AbstractRecursivePass implements Repe
|
|||
$this->repeatedPass = $repeatedPass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all services inlined by this pass.
|
||||
*
|
||||
* @return array Service id strings
|
||||
*/
|
||||
public function getInlinedServiceIds()
|
||||
{
|
||||
return $this->inlinedServiceIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -46,6 +57,7 @@ class InlineServiceDefinitionsPass extends AbstractRecursivePass implements Repe
|
|||
|
||||
if ($this->isInlineableDefinition($id, $definition, $this->container->getCompiler()->getServiceReferenceGraph())) {
|
||||
$this->container->log($this, sprintf('Inlined service "%s" to "%s".', $id, $this->currentId));
|
||||
$this->inlinedServiceIds[] = $id;
|
||||
|
||||
if ($definition->isShared()) {
|
||||
return $definition;
|
||||
|
|
|
@ -73,11 +73,11 @@ class PassConfig
|
|||
new RemoveAbstractDefinitionsPass(),
|
||||
new RepeatedPass(array(
|
||||
new AnalyzeServiceReferencesPass(),
|
||||
new InlineServiceDefinitionsPass(),
|
||||
$inlinedServicePass = new InlineServiceDefinitionsPass(),
|
||||
new AnalyzeServiceReferencesPass(),
|
||||
new RemoveUnusedDefinitionsPass(),
|
||||
)),
|
||||
new AutowireExceptionPass($autowirePass),
|
||||
new AutowireExceptionPass($autowirePass, $inlinedServicePass),
|
||||
new CheckExceptionOnInvalidReferenceBehaviorPass(),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\Definition;
|
|||
* Null dumper, negates any proxy code generation for any given service definition.
|
||||
*
|
||||
* @author Marco Pivetta <ocramius@gmail.com>
|
||||
*
|
||||
* @final since version 3.3
|
||||
*/
|
||||
class NullDumper implements DumperInterface
|
||||
{
|
||||
|
@ -31,7 +33,7 @@ class NullDumper implements DumperInterface
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getProxyFactoryCode(Definition $definition, $id)
|
||||
public function getProxyFactoryCode(Definition $definition, $id, $methodName = null)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Tests\Compiler;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Compiler\AutowireExceptionPass;
|
||||
use Symfony\Component\DependencyInjection\Compiler\AutowirePass;
|
||||
use Symfony\Component\DependencyInjection\Compiler\InlineServiceDefinitionsPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
|
||||
|
||||
|
@ -29,10 +30,45 @@ class AutowireExceptionPassTest extends TestCase
|
|||
->method('getAutowiringExceptions')
|
||||
->will($this->returnValue(array($autowireException)));
|
||||
|
||||
$inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class)
|
||||
->getMock();
|
||||
$inlinePass->expects($this->any())
|
||||
->method('getInlinedServiceIds')
|
||||
->will($this->returnValue(array()));
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('foo_service_id');
|
||||
|
||||
$pass = new AutowireExceptionPass($autowirePass);
|
||||
$pass = new AutowireExceptionPass($autowirePass, $inlinePass);
|
||||
|
||||
try {
|
||||
$pass->process($container);
|
||||
$this->fail('->process() should throw the exception if the service id exists');
|
||||
} catch (\Exception $e) {
|
||||
$this->assertSame($autowireException, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function testThrowExceptionIfServiceInlined()
|
||||
{
|
||||
$autowirePass = $this->getMockBuilder(AutowirePass::class)
|
||||
->getMock();
|
||||
|
||||
$autowireException = new AutowiringFailedException('foo_service_id', 'An autowiring exception message');
|
||||
$autowirePass->expects($this->any())
|
||||
->method('getAutowiringExceptions')
|
||||
->will($this->returnValue(array($autowireException)));
|
||||
|
||||
$inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class)
|
||||
->getMock();
|
||||
$inlinePass->expects($this->any())
|
||||
->method('getInlinedServiceIds')
|
||||
->will($this->returnValue(array('foo_service_id')));
|
||||
|
||||
// don't register the foo_service_id service
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$pass = new AutowireExceptionPass($autowirePass, $inlinePass);
|
||||
|
||||
try {
|
||||
$pass->process($container);
|
||||
|
@ -52,9 +88,15 @@ class AutowireExceptionPassTest extends TestCase
|
|||
->method('getAutowiringExceptions')
|
||||
->will($this->returnValue(array($autowireException)));
|
||||
|
||||
$inlinePass = $this->getMockBuilder(InlineServiceDefinitionsPass::class)
|
||||
->getMock();
|
||||
$inlinePass->expects($this->any())
|
||||
->method('getInlinedServiceIds')
|
||||
->will($this->returnValue(array()));
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$pass = new AutowireExceptionPass($autowirePass);
|
||||
$pass = new AutowireExceptionPass($autowirePass, $inlinePass);
|
||||
|
||||
$pass->process($container);
|
||||
// mark the test as passed
|
||||
|
|
|
@ -252,6 +252,30 @@ class InlineServiceDefinitionsPassTest extends TestCase
|
|||
$this->assertSame('inline', (string) $values[0]);
|
||||
}
|
||||
|
||||
public function testGetInlinedServiceIds()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container
|
||||
->register('inlinable.service')
|
||||
->setPublic(false)
|
||||
;
|
||||
$container
|
||||
->register('non_inlinable.service')
|
||||
->setPublic(true)
|
||||
;
|
||||
|
||||
$container
|
||||
->register('service')
|
||||
->setArguments(array(new Reference('inlinable.service')))
|
||||
;
|
||||
|
||||
$inlinePass = new InlineServiceDefinitionsPass();
|
||||
$repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), $inlinePass));
|
||||
$repeatedPass->process($container);
|
||||
|
||||
$this->assertEquals(array('inlinable.service'), $inlinePass->getInlinedServiceIds());
|
||||
}
|
||||
|
||||
protected function process(ContainerBuilder $container)
|
||||
{
|
||||
$repeatedPass = new RepeatedPass(array(new AnalyzeServiceReferencesPass(), new InlineServiceDefinitionsPass()));
|
||||
|
|
|
@ -88,7 +88,7 @@ class DummyProxyDumper implements ProxyDumper
|
|||
return false;
|
||||
}
|
||||
|
||||
public function getProxyFactoryCode(Definition $definition, $id)
|
||||
public function getProxyFactoryCode(Definition $definition, $id, $methodName = null)
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ CHANGELOG
|
|||
* deprecated the special `SYMFONY__` environment variables
|
||||
* added the possibility to change the query string parameter used by `UriSigner`
|
||||
* deprecated `LazyLoadingFragmentHandler::addRendererService()`
|
||||
* deprecated `Extension::addClassesToCompile()`
|
||||
* deprecated `Extension::addClassesToCompile()` and `Extension::getClassesToCompile()`
|
||||
* deprecated `Psr6CacheClearer::addPool()`
|
||||
|
||||
3.2.0
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
composer.lock
|
||||
phpunit.xml
|
||||
vendor/
|
|
@ -0,0 +1,7 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* added the component
|
|
@ -0,0 +1,21 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* Base ExceptionInterface for the Lock Component.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* LockAcquiringException is thrown when an issue happens during the acquisition of a lock.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LockAcquiringException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* LockConflictedException is thrown when a lock is acquired by someone else.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LockConflictedException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* LockReleasingException is thrown when an issue happens during the release of a lock.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LockReleasingException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* LockStorageException is thrown when an issue happens during the manipulation of a lock in a store.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LockStorageException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* NotSupportedException is thrown when an unsupported method is called.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class NotSupportedException extends \LogicException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?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;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* Factory provides method to create locks.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class Factory implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $store;
|
||||
|
||||
public function __construct(StoreInterface $store)
|
||||
{
|
||||
$this->store = $store;
|
||||
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lock for the given resource.
|
||||
*
|
||||
* @param string $resource The resource to lock
|
||||
* @param float $ttl maximum expected lock duration
|
||||
*
|
||||
* @return Lock
|
||||
*/
|
||||
public function createLock($resource, $ttl = 300.0)
|
||||
{
|
||||
$lock = new Lock(new Key($resource), $this->store, $ttl);
|
||||
$lock->setLogger($this->logger);
|
||||
|
||||
return $lock;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?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;
|
||||
|
||||
/**
|
||||
* Key is a container for the state of the locks in stores.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class Key
|
||||
{
|
||||
private $resource;
|
||||
private $state = array();
|
||||
|
||||
/**
|
||||
* @param string $resource
|
||||
*/
|
||||
public function __construct($resource)
|
||||
{
|
||||
$this->resource = (string) $resource;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return $this->resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stateKey
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasState($stateKey)
|
||||
{
|
||||
return isset($this->state[$stateKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stateKey
|
||||
* @param mixed $state
|
||||
*/
|
||||
public function setState($stateKey, $state)
|
||||
{
|
||||
$this->state[$stateKey] = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $stateKey
|
||||
*/
|
||||
public function removeState($stateKey)
|
||||
{
|
||||
unset($this->state[$stateKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $stateKey
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getState($stateKey)
|
||||
{
|
||||
return $this->state[$stateKey];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2016-2017 Fabien Potencier
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,123 @@
|
|||
<?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;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockAcquiringException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockReleasingException;
|
||||
|
||||
/**
|
||||
* Lock is the default implementation of the LockInterface.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
final class Lock implements LockInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $store;
|
||||
private $key;
|
||||
private $ttl;
|
||||
|
||||
/**
|
||||
* @param Key $key
|
||||
* @param StoreInterface $store
|
||||
* @param float|null $ttl
|
||||
*/
|
||||
public function __construct(Key $key, StoreInterface $store, $ttl = null)
|
||||
{
|
||||
$this->store = $store;
|
||||
$this->key = $key;
|
||||
$this->ttl = $ttl;
|
||||
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function acquire($blocking = false)
|
||||
{
|
||||
try {
|
||||
if (!$blocking) {
|
||||
$this->store->save($this->key);
|
||||
} else {
|
||||
$this->store->waitAndSave($this->key);
|
||||
}
|
||||
|
||||
$this->logger->info('Successfully acquired the "{resource}" lock.', array('resource' => $this->key));
|
||||
|
||||
if ($this->ttl) {
|
||||
$this->refresh();
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (LockConflictedException $e) {
|
||||
$this->logger->warning('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', array('resource' => $this->key));
|
||||
|
||||
if ($blocking) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Failed to acquire the "{resource}" lock.', array('resource' => $this->key, 'exception' => $e));
|
||||
throw new LockAcquiringException(sprintf('Failed to acquire the "%s" lock.', $this->key), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function refresh()
|
||||
{
|
||||
if (!$this->ttl) {
|
||||
throw new InvalidArgumentException('You have to define an expiration duration.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->store->putOffExpiration($this->key, $this->ttl);
|
||||
$this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', array('resource' => $this->key, 'ttl' => $this->ttl));
|
||||
} catch (LockConflictedException $e) {
|
||||
$this->logger->warning('Failed to define an expiration for the "{resource}" lock, someone else acquired the lock.', array('resource' => $this->key));
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('Failed to define an expiration for the "{resource}" lock.', array('resource' => $this->key, 'exception' => $e));
|
||||
throw new LockAcquiringException(sprintf('Failed to define an expiration for the "%s" lock.', $this->key), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isAcquired()
|
||||
{
|
||||
return $this->store->exists($this->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function release()
|
||||
{
|
||||
$this->store->delete($this->key);
|
||||
|
||||
if ($this->store->exists($this->key)) {
|
||||
$this->logger->warning('Failed to release the "{resource}" lock.', array('resource' => $this->key));
|
||||
throw new LockReleasingException(sprintf('Failed to release the "%s" lock.', $this->key));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?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;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockAcquiringException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockReleasingException;
|
||||
|
||||
/**
|
||||
* LockInterface defines an interface to manipulate the status of a lock.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface LockInterface
|
||||
{
|
||||
/**
|
||||
* Acquires the lock. If the lock is acquired by someone else, the parameter `blocking` determines whether or not
|
||||
* the the call should block until the release of the lock.
|
||||
*
|
||||
* @param bool $blocking Whether or not the Lock should wait for the release of someone else
|
||||
*
|
||||
* @return bool Whether or not the lock had been acquired.
|
||||
*
|
||||
* @throws LockConflictedException If the lock is acquired by someone else in blocking mode
|
||||
* @throws LockAcquiringException If the lock can not be acquired
|
||||
*/
|
||||
public function acquire($blocking = false);
|
||||
|
||||
/**
|
||||
* Increase the duration of an acquired lock.
|
||||
*
|
||||
* @throws LockConflictedException If the lock is acquired by someone else
|
||||
* @throws LockAcquiringException If the lock can not be refreshed
|
||||
*/
|
||||
public function refresh();
|
||||
|
||||
/**
|
||||
* Returns whether or not the lock is acquired.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAcquired();
|
||||
|
||||
/**
|
||||
* Release the lock.
|
||||
*
|
||||
* @throws LockReleasingException If the lock can not be released
|
||||
*/
|
||||
public function release();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
Lock Component
|
||||
==============
|
||||
|
||||
Resources
|
||||
---------
|
||||
|
||||
* [Documentation](https://symfony.com/doc/master/components/lock.html)
|
||||
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||
in the [main Symfony repository](https://github.com/symfony/symfony)
|
|
@ -0,0 +1,174 @@
|
|||
<?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 Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* CombinedStore is a StoreInterface implementation able to manage and synchronize several StoreInterfaces.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CombinedStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/** @var StoreInterface[] */
|
||||
private $stores;
|
||||
/** @var StrategyInterface */
|
||||
private $strategy;
|
||||
|
||||
/**
|
||||
* @param StoreInterface[] $stores The list of synchronized stores
|
||||
* @param StrategyInterface $strategy
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __construct(array $stores, StrategyInterface $strategy)
|
||||
{
|
||||
foreach ($stores as $store) {
|
||||
if (!$store instanceof StoreInterface) {
|
||||
throw new InvalidArgumentException(sprintf('The store must implement "%s". Got "%s".', StoreInterface::class, get_class($store)));
|
||||
}
|
||||
}
|
||||
|
||||
$this->stores = $stores;
|
||||
$this->strategy = $strategy;
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
$store->save($key);
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('One store failed to save the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Quorum has not been met.', array('resource' => $key, 'success' => $successCount, 'failure' => $failureCount));
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new NotSupportedException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
$store->putOffExpiration($key, $ttl);
|
||||
++$successCount;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->warning('Failed to define the expiration for the "{resource}" lock. Quorum has not been met.', array('resource' => $key, 'success' => $successCount, 'failure' => $failureCount));
|
||||
|
||||
// clean up potential locks
|
||||
$this->delete($key);
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
foreach ($this->stores as $store) {
|
||||
try {
|
||||
$store->delete($key);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->notice('One store failed to delete the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->notice('One store failed to delete the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
$successCount = 0;
|
||||
$failureCount = 0;
|
||||
$storesCount = count($this->stores);
|
||||
|
||||
foreach ($this->stores as $store) {
|
||||
if ($store->exists($key)) {
|
||||
++$successCount;
|
||||
} else {
|
||||
++$failureCount;
|
||||
}
|
||||
|
||||
if ($this->strategy->isMet($successCount, $storesCount)) {
|
||||
return true;
|
||||
}
|
||||
if (!$this->strategy->canBeMet($failureCount, $storesCount)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<?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;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\LockStorageException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* FlockStore is a StoreInterface implementation using the FileSystem flock.
|
||||
*
|
||||
* Original implementation in \Symfony\Component\Filesystem\LockHandler.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
* @author Grégoire Pineau <lyrixx@lyrixx.info>
|
||||
* @author Romain Neutron <imprec@gmail.com>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
class FlockStore implements StoreInterface
|
||||
{
|
||||
private $lockPath;
|
||||
|
||||
/**
|
||||
* @param string $lockPath the directory to store the lock
|
||||
*
|
||||
* @throws LockStorageException If the lock directory could not be created or is not writable
|
||||
*/
|
||||
public function __construct($lockPath)
|
||||
{
|
||||
if (!is_dir($lockPath) || !is_writable($lockPath)) {
|
||||
throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $lockPath));
|
||||
}
|
||||
|
||||
$this->lockPath = $lockPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->lock($key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, $blocking)
|
||||
{
|
||||
// The lock is maybe already acquired.
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fileName = sprintf('%s/sf.%s.%s.lock',
|
||||
$this->lockPath,
|
||||
preg_replace('/[^a-z0-9\._-]+/i', '-', $key),
|
||||
hash('sha256', $key)
|
||||
);
|
||||
|
||||
// Silence error reporting
|
||||
set_error_handler(function () {
|
||||
});
|
||||
if (!$handle = fopen($fileName, 'r')) {
|
||||
if ($handle = fopen($fileName, 'x')) {
|
||||
chmod($fileName, 0444);
|
||||
} elseif (!$handle = fopen($fileName, 'r')) {
|
||||
usleep(100); // Give some time for chmod() to complete
|
||||
$handle = fopen($fileName, 'r');
|
||||
}
|
||||
}
|
||||
restore_error_handler();
|
||||
|
||||
if (!$handle) {
|
||||
$error = error_get_last();
|
||||
throw new LockStorageException($error['message'], 0, null);
|
||||
}
|
||||
|
||||
// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
|
||||
// https://bugs.php.net/54129
|
||||
if (!flock($handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
|
||||
fclose($handle);
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$key->setState(__CLASS__, $handle);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
// do nothing, the flock locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// The lock is maybe not acquired.
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = $key->getState(__CLASS__);
|
||||
|
||||
flock($handle, LOCK_UN | LOCK_NB);
|
||||
fclose($handle);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $key->hasState(__CLASS__);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
<?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;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* MemcachedStore is a StoreInterface implementation using Memcached as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class MemcachedStore implements StoreInterface
|
||||
{
|
||||
private $memcached;
|
||||
private $initialTtl;
|
||||
/** @var bool */
|
||||
private $useExtendedReturn;
|
||||
|
||||
public static function isSupported()
|
||||
{
|
||||
return extension_loaded('memcached');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Memcached $memcached
|
||||
* @param int $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct(\Memcached $memcached, $initialTtl = 300)
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Memcached extension is required');
|
||||
}
|
||||
|
||||
if ($initialTtl < 1) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->memcached = $memcached;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$token = $this->getToken($key);
|
||||
|
||||
if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the lock is already acquire. It could be us. Let's try to put off.
|
||||
$this->putOffExpiration($key, $this->initialTtl);
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
if ($ttl < 1) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
|
||||
}
|
||||
|
||||
// Interface defines a float value but Store required an integer.
|
||||
$ttl = (int) ceil($ttl);
|
||||
|
||||
$token = $this->getToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
|
||||
// Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
|
||||
if (\Memcached::RES_NOTFOUND === $this->memcached->getResultCode()) {
|
||||
if ($this->memcached->add((string) $key, $token, $ttl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no luck, with concurrency, someone else acquire the lock
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
// Someone else steal the lock
|
||||
if ($value !== $token) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
if (!$this->memcached->cas($cas, (string) $key, $token, $ttl)) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$token = $this->getToken($key);
|
||||
|
||||
list($value, $cas) = $this->getValueAndCas($key);
|
||||
|
||||
if ($value !== $token) {
|
||||
// we are not the owner of the lock. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// To avoid concurrency in deletion, the trick is to extends the TTL then deleting the key
|
||||
if (!$this->memcached->cas($cas, (string) $key, $token, 2)) {
|
||||
// Someone steal our lock. It does not belongs to us anymore. Nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we are the owner of the lock for 2 more seconds, we can delete it.
|
||||
$this->memcached->delete((string) $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->memcached->get((string) $key) === $this->getToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an unique token for the given key.
|
||||
*
|
||||
* @param Key $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getToken(Key $key)
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
|
||||
private function getValueAndCas(Key $key)
|
||||
{
|
||||
if (null === $this->useExtendedReturn) {
|
||||
$this->useExtendedReturn = version_compare(phpversion('memcached'), '2.9.9', '>');
|
||||
}
|
||||
|
||||
if ($this->useExtendedReturn) {
|
||||
$extendedReturn = $this->memcached->get((string) $key, null, \Memcached::GET_EXTENDED);
|
||||
if ($extendedReturn === \Memcached::GET_ERROR_RETURN_VALUE) {
|
||||
return array($extendedReturn, 0.0);
|
||||
}
|
||||
|
||||
return array($extendedReturn['value'], $extendedReturn['cas']);
|
||||
}
|
||||
|
||||
$cas = 0.0;
|
||||
$value = $this->memcached->get((string) $key, null, $cas);
|
||||
|
||||
return array($value, $cas);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
<?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;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* RedisStore is a StoreInterface implementation using Redis as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RedisStore implements StoreInterface
|
||||
{
|
||||
private $redis;
|
||||
private $initialTtl;
|
||||
|
||||
/**
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
|
||||
* @param float $initialTtl the expiration delay of locks in seconds
|
||||
*/
|
||||
public function __construct($redisClient, $initialTtl = 300.0)
|
||||
{
|
||||
if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
|
||||
}
|
||||
|
||||
if ($initialTtl <= 0) {
|
||||
throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
|
||||
}
|
||||
|
||||
$this->redis = $redisClient;
|
||||
$this->initialTtl = $initialTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
||||
else
|
||||
return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
|
||||
end
|
||||
';
|
||||
|
||||
$expire = (int) ceil($this->initialTtl * 1000);
|
||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
}
|
||||
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
';
|
||||
|
||||
$expire = (int) ceil($ttl * 1000);
|
||||
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$script = '
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
';
|
||||
|
||||
$this->evaluate($script, (string) $key, array($this->getToken($key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->redis->get((string) $key) === $this->getToken($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a script in the corresponding redis client.
|
||||
*
|
||||
* @param string $script
|
||||
* @param string $resource
|
||||
* @param array $args
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function evaluate($script, $resource, array $args)
|
||||
{
|
||||
if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster) {
|
||||
return $this->redis->eval($script, array_merge(array($resource), $args), 1);
|
||||
}
|
||||
|
||||
if ($this->redis instanceof \RedisArray) {
|
||||
return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge(array($resource), $args), 1);
|
||||
}
|
||||
|
||||
if ($this->redis instanceof \Predis\Client) {
|
||||
return call_user_func_array(array($this->redis, 'eval'), array_merge(array($script, 1, $resource), $args));
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('%s() expects been initialized with a Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($this->redis) ? get_class($this->redis) : gettype($this->redis)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an unique token for the given key.
|
||||
*
|
||||
* @param Key $key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getToken(Key $key)
|
||||
{
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
$token = base64_encode(random_bytes(32));
|
||||
$key->setState(__CLASS__, $token);
|
||||
}
|
||||
|
||||
return $key->getState(__CLASS__);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
<?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 Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a
|
||||
* blocking storage.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
private $decorated;
|
||||
private $retrySleep;
|
||||
private $retryCount;
|
||||
|
||||
/**
|
||||
* @param StoreInterface $decorated The decorated StoreInterface
|
||||
* @param int $retrySleep Duration in ms between 2 retry
|
||||
* @param int $retryCount Maximum amount of retry
|
||||
*/
|
||||
public function __construct(StoreInterface $decorated, $retrySleep = 100, $retryCount = PHP_INT_MAX)
|
||||
{
|
||||
$this->decorated = $decorated;
|
||||
$this->retrySleep = $retrySleep;
|
||||
$this->retryCount = $retryCount;
|
||||
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->decorated->save($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$retry = 0;
|
||||
$sleepRandomness = (int) ($this->retrySleep / 10);
|
||||
do {
|
||||
try {
|
||||
$this->decorated->save($key);
|
||||
|
||||
return;
|
||||
} catch (LockConflictedException $e) {
|
||||
usleep(($this->retrySleep + random_int(-$sleepRandomness, $sleepRandomness)) * 1000);
|
||||
}
|
||||
} while (++$retry < $this->retryCount);
|
||||
|
||||
$this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', array('resource' => $key, 'retry' => $retry));
|
||||
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
$this->decorated->putOffExpiration($key, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
$this->decorated->delete($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $this->decorated->exists($key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?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;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* SemaphoreStore is a StoreInterface implementation using Semaphore as store engine.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SemaphoreStore implements StoreInterface
|
||||
{
|
||||
public static function isSupported()
|
||||
{
|
||||
return extension_loaded('sysvsem');
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (!static::isSupported()) {
|
||||
throw new InvalidArgumentException('Semaphore extension (sysvsem) is required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Key $key)
|
||||
{
|
||||
$this->lock($key, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function waitAndSave(Key $key)
|
||||
{
|
||||
$this->lock($key, true);
|
||||
}
|
||||
|
||||
private function lock(Key $key, $blocking)
|
||||
{
|
||||
if ($key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = sem_get(crc32($key));
|
||||
$acquired = sem_acquire($resource, !$blocking);
|
||||
|
||||
if (!$acquired) {
|
||||
throw new LockConflictedException();
|
||||
}
|
||||
|
||||
$key->setState(__CLASS__, $resource);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(Key $key)
|
||||
{
|
||||
// The lock is maybe not acquired.
|
||||
if (!$key->hasState(__CLASS__)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resource = $key->getState(__CLASS__);
|
||||
|
||||
sem_release($resource);
|
||||
|
||||
$key->removeState(__CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl)
|
||||
{
|
||||
// do nothing, the flock locks forever.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function exists(Key $key)
|
||||
{
|
||||
return $key->hasState(__CLASS__);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?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;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Exception\NotSupportedException;
|
||||
|
||||
/**
|
||||
* StoreInterface defines an interface to manipulate a lock store.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface StoreInterface
|
||||
{
|
||||
/**
|
||||
* Stores the resource if it's not locked by someone else.
|
||||
*
|
||||
* @param Key $key key to lock
|
||||
*
|
||||
* @throws LockConflictedException
|
||||
*/
|
||||
public function save(Key $key);
|
||||
|
||||
/**
|
||||
* Waits a key becomes free, then stores the resource.
|
||||
*
|
||||
* If the store does not support this feature it should throw a NotSupportedException.
|
||||
*
|
||||
* @param Key $key key to lock
|
||||
*
|
||||
* @throws LockConflictedException
|
||||
* @throws NotSupportedException
|
||||
*/
|
||||
public function waitAndSave(Key $key);
|
||||
|
||||
/**
|
||||
* Extends the ttl of a resource.
|
||||
*
|
||||
* If the store does not support this feature it should throw a NotSupportedException.
|
||||
*
|
||||
* @param Key $key key to lock
|
||||
* @param float $ttl amount of second to keep the lock in the store
|
||||
*
|
||||
* @throws LockConflictedException
|
||||
* @throws NotSupportedException
|
||||
*/
|
||||
public function putOffExpiration(Key $key, $ttl);
|
||||
|
||||
/**
|
||||
* Removes a resource from the storage.
|
||||
*
|
||||
* @param Key $key key to remove
|
||||
*/
|
||||
public function delete(Key $key);
|
||||
|
||||
/**
|
||||
* Returns whether or not the resource exists in the storage.
|
||||
*
|
||||
* @param Key $key key to remove
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(Key $key);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?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\Strategy;
|
||||
|
||||
/**
|
||||
* ConsensusStrategy is a StrategyInterface implementation where strictly more than 50% items should be successful.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class ConsensusStrategy implements StrategyInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isMet($numberOfSuccess, $numberOfItems)
|
||||
{
|
||||
return $numberOfSuccess > ($numberOfItems / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canBeMet($numberOfFailure, $numberOfItems)
|
||||
{
|
||||
return $numberOfFailure < ($numberOfItems / 2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?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\Strategy;
|
||||
|
||||
/**
|
||||
* StrategyInterface defines an interface to indicate when a quorum is met and can be met.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface StrategyInterface
|
||||
{
|
||||
/**
|
||||
* Returns whether or not the quorum is met.
|
||||
*
|
||||
* @param int $numberOfSuccess
|
||||
* @param int $numberOfItems
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isMet($numberOfSuccess, $numberOfItems);
|
||||
|
||||
/**
|
||||
* Returns whether or not the quorum *could* be met.
|
||||
*
|
||||
* This method does not mean the quorum *would* be met for sure, but can be useful to stop a process early when you
|
||||
* known there is no chance to meet the quorum.
|
||||
*
|
||||
* @param int $numberOfFailure
|
||||
* @param int $numberOfItems
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canBeMet($numberOfFailure, $numberOfItems);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?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\Strategy;
|
||||
|
||||
/**
|
||||
* UnanimousStrategy is a StrategyInterface implementation where 100% of elements should be successful.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class UnanimousStrategy implements StrategyInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isMet($numberOfSuccess, $numberOfItems)
|
||||
{
|
||||
return $numberOfSuccess === $numberOfItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canBeMet($numberOfFailure, $numberOfItems)
|
||||
{
|
||||
return $numberOfFailure === 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?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\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Lock\Factory;
|
||||
use Symfony\Component\Lock\LockInterface;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class FactoryTest extends TestCase
|
||||
{
|
||||
public function testCreateLock()
|
||||
{
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
|
||||
$factory = new Factory($store);
|
||||
$factory->setLogger($logger);
|
||||
|
||||
$lock = $factory->createLock('foo');
|
||||
|
||||
$this->assertInstanceOf(LockInterface::class, $lock);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
<?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\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\Lock;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LockTest extends TestCase
|
||||
{
|
||||
public function testAcquireNoBlocking()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('save');
|
||||
|
||||
$this->assertTrue($lock->acquire(false));
|
||||
}
|
||||
|
||||
public function testAcquireReturnsFalse()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->willThrowException(new LockConflictedException());
|
||||
|
||||
$this->assertFalse($lock->acquire(false));
|
||||
}
|
||||
|
||||
public function testAcquireBlocking()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store);
|
||||
|
||||
$store
|
||||
->expects($this->never())
|
||||
->method('save');
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('waitAndSave');
|
||||
|
||||
$this->assertTrue($lock->acquire(true));
|
||||
}
|
||||
|
||||
public function testAcquireSetsTtl()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store, 10);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('save');
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, 10);
|
||||
|
||||
$lock->acquire();
|
||||
}
|
||||
|
||||
public function testRefresh()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store, 10);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, 10);
|
||||
|
||||
$lock->refresh();
|
||||
}
|
||||
|
||||
public function testIsAquired()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store, 10);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('exists')
|
||||
->with($key)
|
||||
->willReturn(true);
|
||||
|
||||
$this->assertTrue($lock->isAcquired());
|
||||
}
|
||||
|
||||
public function testRelease()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store, 10);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with($key);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('exists')
|
||||
->with($key)
|
||||
->willReturn(false);
|
||||
|
||||
$lock->release();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Lock\Exception\LockReleasingException
|
||||
*/
|
||||
public function testReleaseThrowsExceptionIfNotWellDeleted()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$lock = new Lock($key, $store, 10);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with($key);
|
||||
|
||||
$store
|
||||
->expects($this->once())
|
||||
->method('exists')
|
||||
->with($key)
|
||||
->willReturn(true);
|
||||
|
||||
$lock->release();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Store\RedisStore;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
abstract class AbstractRedisStoreTest extends AbstractStoreTest
|
||||
{
|
||||
use ExpiringStoreTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getClockDelay()
|
||||
{
|
||||
return 250000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a RedisConnection.
|
||||
*
|
||||
* @return \Redis|\RedisArray|\RedisCluster|\Predis\Client
|
||||
*/
|
||||
abstract protected function getRedisConnection();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStore()
|
||||
{
|
||||
return new RedisStore($this->getRedisConnection());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
abstract class AbstractStoreTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @return StoreInterface;
|
||||
*/
|
||||
abstract protected function getStore();
|
||||
|
||||
public function testSave()
|
||||
{
|
||||
$store = $this->getStore();
|
||||
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->assertFalse($store->exists($key));
|
||||
$store->save($key);
|
||||
$this->assertTrue($store->exists($key));
|
||||
$store->delete($key);
|
||||
$this->assertFalse($store->exists($key));
|
||||
}
|
||||
|
||||
public function testSaveWithDifferentResources()
|
||||
{
|
||||
$store = $this->getStore();
|
||||
|
||||
$key1 = new Key(uniqid(__METHOD__, true));
|
||||
$key2 = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$store->save($key1);
|
||||
$this->assertTrue($store->exists($key1));
|
||||
$this->assertFalse($store->exists($key2));
|
||||
$store->save($key2);
|
||||
|
||||
$this->assertTrue($store->exists($key1));
|
||||
$this->assertTrue($store->exists($key2));
|
||||
|
||||
$store->delete($key1);
|
||||
$this->assertFalse($store->exists($key1));
|
||||
$store->delete($key2);
|
||||
$this->assertFalse($store->exists($key2));
|
||||
}
|
||||
|
||||
public function testSaveWithDifferentKeysOnSameResources()
|
||||
{
|
||||
$store = $this->getStore();
|
||||
|
||||
$resource = uniqid(__METHOD__, true);
|
||||
$key1 = new Key($resource);
|
||||
$key2 = new Key($resource);
|
||||
|
||||
$store->save($key1);
|
||||
$this->assertTrue($store->exists($key1));
|
||||
$this->assertFalse($store->exists($key2));
|
||||
|
||||
try {
|
||||
$store->save($key2);
|
||||
throw new \Exception('The store shouldn\'t save the second key');
|
||||
} catch (LockConflictedException $e) {
|
||||
}
|
||||
|
||||
// The failure of previous attempt should not impact the state of current locks
|
||||
$this->assertTrue($store->exists($key1));
|
||||
$this->assertFalse($store->exists($key2));
|
||||
|
||||
$store->delete($key1);
|
||||
$this->assertFalse($store->exists($key1));
|
||||
$this->assertFalse($store->exists($key2));
|
||||
|
||||
$store->save($key2);
|
||||
$this->assertFalse($store->exists($key1));
|
||||
$this->assertTrue($store->exists($key2));
|
||||
|
||||
$store->delete($key2);
|
||||
$this->assertFalse($store->exists($key1));
|
||||
$this->assertFalse($store->exists($key2));
|
||||
}
|
||||
|
||||
public function testSaveTwice()
|
||||
{
|
||||
$store = $this->getStore();
|
||||
|
||||
$resource = uniqid(__METHOD__, true);
|
||||
$key = new Key($resource);
|
||||
|
||||
$store->save($key);
|
||||
$store->save($key);
|
||||
// just asserts it don't throw an exception
|
||||
$this->addToAssertionCount(1);
|
||||
|
||||
$store->delete($key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
trait BlockingStoreTestTrait
|
||||
{
|
||||
/**
|
||||
* @see AbstractStoreTest::getStore()
|
||||
*/
|
||||
abstract protected function getStore();
|
||||
|
||||
/**
|
||||
* Tests blocking locks thanks to pcntl.
|
||||
*
|
||||
* This test is time sensible: the $clockDelay could be adjust.
|
||||
*
|
||||
* @requires extension pcntl
|
||||
*/
|
||||
public function testBlockingLocks()
|
||||
{
|
||||
// Amount a microsecond used to order async actions
|
||||
$clockDelay = 50000;
|
||||
|
||||
/** @var StoreInterface $store */
|
||||
$store = $this->getStore();
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
if ($childPID1 = pcntl_fork()) {
|
||||
// give time to fork to start
|
||||
usleep(2 * $clockDelay);
|
||||
|
||||
try {
|
||||
// This call should failed given the lock should already by acquired by the child #1
|
||||
$store->save($key);
|
||||
$this->fail('The store saves a locked key.');
|
||||
} catch (LockConflictedException $e) {
|
||||
}
|
||||
|
||||
// This call should be blocked by the child #1
|
||||
$store->waitAndSave($key);
|
||||
$this->assertTrue($store->exists($key));
|
||||
$store->delete($key);
|
||||
|
||||
// Now, assert the child process worked well
|
||||
pcntl_waitpid($childPID1, $status1);
|
||||
$this->assertSame(0, pcntl_wexitstatus($status1), 'The child process couldn\'t lock the resource');
|
||||
} else {
|
||||
try {
|
||||
$store->save($key);
|
||||
// Wait 3 ClockDelay to let parent process to finish
|
||||
usleep(3 * $clockDelay);
|
||||
$store->delete($key);
|
||||
exit(0);
|
||||
} catch (\Exception $e) {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,356 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Exception\LockConflictedException;
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\Strategy\UnanimousStrategy;
|
||||
use Symfony\Component\Lock\Strategy\StrategyInterface;
|
||||
use Symfony\Component\Lock\Store\CombinedStore;
|
||||
use Symfony\Component\Lock\Store\RedisStore;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class CombinedStoreTest extends AbstractStoreTest
|
||||
{
|
||||
use ExpiringStoreTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getClockDelay()
|
||||
{
|
||||
return 250000;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStore()
|
||||
{
|
||||
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
|
||||
try {
|
||||
$redis->connect();
|
||||
} catch (\Exception $e) {
|
||||
self::markTestSkipped($e->getMessage());
|
||||
}
|
||||
|
||||
return new CombinedStore(array(new RedisStore($redis)), new UnanimousStrategy());
|
||||
}
|
||||
|
||||
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
||||
private $strategy;
|
||||
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
||||
private $store1;
|
||||
/** @var \PHPUnit_Framework_MockObject_MockObject */
|
||||
private $store2;
|
||||
/** @var CombinedStore */
|
||||
private $store;
|
||||
|
||||
public function setup()
|
||||
{
|
||||
$this->strategy = $this->getMockBuilder(StrategyInterface::class)->getMock();
|
||||
$this->store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$this->store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
|
||||
$this->store = new CombinedStore(array($this->store1, $this->store2), $this->strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Lock\Exception\LockConflictedException
|
||||
*/
|
||||
public function testSaveThrowsExceptionOnFailure()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->with($key)
|
||||
->willThrowException(new LockConflictedException());
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->with($key)
|
||||
->willThrowException(new LockConflictedException());
|
||||
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('canBeMet')
|
||||
->willReturn(true);
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
$this->store->save($key);
|
||||
}
|
||||
|
||||
public function testSaveCleanupOnFailure()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->with($key)
|
||||
->willThrowException(new LockConflictedException());
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->with($key)
|
||||
->willThrowException(new LockConflictedException());
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('delete');
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('delete');
|
||||
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('canBeMet')
|
||||
->willReturn(true);
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
try {
|
||||
$this->store->save($key);
|
||||
} catch (LockConflictedException $e) {
|
||||
// Catch the exception given this is not what we want to assert in this tests
|
||||
}
|
||||
}
|
||||
|
||||
public function testSaveAbortWhenStrategyCantBeMet()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('save')
|
||||
->with($key)
|
||||
->willThrowException(new LockConflictedException());
|
||||
$this->store2
|
||||
->expects($this->never())
|
||||
->method('save');
|
||||
|
||||
$this->strategy
|
||||
->expects($this->once())
|
||||
->method('canBeMet')
|
||||
->willReturn(false);
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
try {
|
||||
$this->store->save($key);
|
||||
} catch (LockConflictedException $e) {
|
||||
// Catch the exception given this is not what we want to assert in this tests
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Lock\Exception\LockConflictedException
|
||||
*/
|
||||
public function testputOffExpirationThrowsExceptionOnFailure()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$ttl = random_int(1, 10);
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, $ttl)
|
||||
->willThrowException(new LockConflictedException());
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, $ttl)
|
||||
->willThrowException(new LockConflictedException());
|
||||
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('canBeMet')
|
||||
->willReturn(true);
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
$this->store->putOffExpiration($key, $ttl);
|
||||
}
|
||||
|
||||
public function testputOffExpirationCleanupOnFailure()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$ttl = random_int(1, 10);
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, $ttl)
|
||||
->willThrowException(new LockConflictedException());
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, $ttl)
|
||||
->willThrowException(new LockConflictedException());
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('delete');
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('delete');
|
||||
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('canBeMet')
|
||||
->willReturn(true);
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
try {
|
||||
$this->store->putOffExpiration($key, $ttl);
|
||||
} catch (LockConflictedException $e) {
|
||||
// Catch the exception given this is not what we want to assert in this tests
|
||||
}
|
||||
}
|
||||
|
||||
public function testputOffExpirationAbortWhenStrategyCantBeMet()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$ttl = random_int(1, 10);
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('putOffExpiration')
|
||||
->with($key, $ttl)
|
||||
->willThrowException(new LockConflictedException());
|
||||
$this->store2
|
||||
->expects($this->never())
|
||||
->method('putOffExpiration');
|
||||
|
||||
$this->strategy
|
||||
->expects($this->once())
|
||||
->method('canBeMet')
|
||||
->willReturn(false);
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
try {
|
||||
$this->store->putOffExpiration($key, $ttl);
|
||||
} catch (LockConflictedException $e) {
|
||||
// Catch the exception given this is not what we want to assert in this tests
|
||||
}
|
||||
}
|
||||
|
||||
public function testPutOffExpirationIgnoreNonExpiringStorage()
|
||||
{
|
||||
$store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
$store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
|
||||
|
||||
$store = new CombinedStore(array($store1, $store2), $this->strategy);
|
||||
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$ttl = random_int(1, 10);
|
||||
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('canBeMet')
|
||||
->willReturn(true);
|
||||
$this->strategy
|
||||
->expects($this->once())
|
||||
->method('isMet')
|
||||
->with(2, 2)
|
||||
->willReturn(true);
|
||||
|
||||
$store->putOffExpiration($key, $ttl);
|
||||
}
|
||||
|
||||
public function testExistsDontAskToEveryBody()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->store1
|
||||
->expects($this->any())
|
||||
->method('exists')
|
||||
->with($key)
|
||||
->willReturn(false);
|
||||
$this->store2
|
||||
->expects($this->never())
|
||||
->method('exists');
|
||||
|
||||
$this->strategy
|
||||
->expects($this->any())
|
||||
->method('canBeMet')
|
||||
->willReturn(true);
|
||||
$this->strategy
|
||||
->expects($this->once())
|
||||
->method('isMet')
|
||||
->willReturn(true);
|
||||
|
||||
$this->assertTrue($this->store->exists($key));
|
||||
}
|
||||
|
||||
public function testExistsAbortWhenStrategyCantBeMet()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->store1
|
||||
->expects($this->any())
|
||||
->method('exists')
|
||||
->with($key)
|
||||
->willReturn(false);
|
||||
$this->store2
|
||||
->expects($this->never())
|
||||
->method('exists');
|
||||
|
||||
$this->strategy
|
||||
->expects($this->once())
|
||||
->method('canBeMet')
|
||||
->willReturn(false);
|
||||
$this->strategy
|
||||
->expects($this->once())
|
||||
->method('isMet')
|
||||
->willReturn(false);
|
||||
|
||||
$this->assertFalse($this->store->exists($key));
|
||||
}
|
||||
|
||||
public function testDeleteDontStopOnFailure()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
$this->store1
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with($key)
|
||||
->willThrowException(new \Exception());
|
||||
$this->store2
|
||||
->expects($this->once())
|
||||
->method('delete')
|
||||
->with($key);
|
||||
|
||||
$this->store->delete($key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\StoreInterface;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
trait ExpiringStoreTestTrait
|
||||
{
|
||||
/**
|
||||
* Amount a microsecond used to order async actions.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
abstract protected function getClockDelay();
|
||||
|
||||
/**
|
||||
* @see AbstractStoreTest::getStore()
|
||||
*/
|
||||
abstract protected function getStore();
|
||||
|
||||
/**
|
||||
* Tests the store automatically delete the key when it expire.
|
||||
*
|
||||
* This test is time sensible: the $clockDelay could be adjust.
|
||||
*/
|
||||
public function testExpiration()
|
||||
{
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
$clockDelay = $this->getClockDelay();
|
||||
|
||||
/** @var StoreInterface $store */
|
||||
$store = $this->getStore();
|
||||
|
||||
$store->save($key);
|
||||
$store->putOffExpiration($key, $clockDelay / 1000000);
|
||||
$this->assertTrue($store->exists($key));
|
||||
|
||||
usleep(2 * $clockDelay);
|
||||
$this->assertFalse($store->exists($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the refresh can push the limits to the expiration.
|
||||
*
|
||||
* This test is time sensible: the $clockDelay could be adjust.
|
||||
*/
|
||||
public function testRefreshLock()
|
||||
{
|
||||
// Amount a microsecond used to order async actions
|
||||
$clockDelay = $this->getClockDelay();
|
||||
|
||||
// Amount a microsecond used to order async actions
|
||||
$key = new Key(uniqid(__METHOD__, true));
|
||||
|
||||
/** @var StoreInterface $store */
|
||||
$store = $this->getStore();
|
||||
|
||||
$store->save($key);
|
||||
$store->putOffExpiration($key, 1.0 * $clockDelay / 1000000);
|
||||
$this->assertTrue($store->exists($key));
|
||||
|
||||
usleep(2.1 * $clockDelay);
|
||||
$this->assertFalse($store->exists($key));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Key;
|
||||
use Symfony\Component\Lock\Store\FlockStore;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class FlockStoreTest extends AbstractStoreTest
|
||||
{
|
||||
use BlockingStoreTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getStore()
|
||||
{
|
||||
return new FlockStore(sys_get_temp_dir());
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Lock\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage The directory "/a/b/c/d/e" is not writable.
|
||||
*/
|
||||
public function testConstructWhenRepositoryDoesNotExist()
|
||||
{
|
||||
if (!getenv('USER') || 'root' === getenv('USER')) {
|
||||
$this->markTestSkipped('This test will fail if run under superuser');
|
||||
}
|
||||
|
||||
new FlockStore('/a/b/c/d/e');
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\Lock\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage The directory "/" is not writable.
|
||||
*/
|
||||
public function testConstructWhenRepositoryIsNotWriteable()
|
||||
{
|
||||
if (!getenv('USER') || 'root' === getenv('USER')) {
|
||||
$this->markTestSkipped('This test will fail if run under superuser');
|
||||
}
|
||||
|
||||
new FlockStore('/');
|
||||
}
|
||||
|
||||
public function testSaveSanitizeName()
|
||||
{
|
||||
$store = $this->getStore();
|
||||
|
||||
$key = new Key('<?php echo "% hello word ! %" ?>');
|
||||
|
||||
$file = sprintf(
|
||||
'%s/sf.-php-echo-hello-word-.4b3d9d0d27ddef3a78a64685dda3a963e478659a9e5240feaf7b4173a8f28d5f.lock',
|
||||
sys_get_temp_dir()
|
||||
);
|
||||
// ensure the file does not exist before the store
|
||||
@unlink($file);
|
||||
|
||||
$store->save($key);
|
||||
|
||||
$this->assertFileExists($file);
|
||||
|
||||
$store->delete($key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Store\MemcachedStore;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @requires extension memcached
|
||||
*/
|
||||
class MemcachedStoreTest extends AbstractStoreTest
|
||||
{
|
||||
use ExpiringStoreTestTrait;
|
||||
|
||||
public static function setupBeforeClass()
|
||||
{
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer(getenv('MEMCACHED_HOST'), 11211);
|
||||
if (false === $memcached->getStats()) {
|
||||
self::markTestSkipped('Unable to connect to the memcache host');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getClockDelay()
|
||||
{
|
||||
return 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStore()
|
||||
{
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer(getenv('MEMCACHED_HOST'), 11211);
|
||||
|
||||
return new MemcachedStore($memcached);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class PredisStoreTest extends AbstractRedisStoreTest
|
||||
{
|
||||
public static function setupBeforeClass()
|
||||
{
|
||||
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
|
||||
try {
|
||||
$redis->connect();
|
||||
} catch (\Exception $e) {
|
||||
self::markTestSkipped($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRedisConnection()
|
||||
{
|
||||
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
|
||||
$redis->connect();
|
||||
|
||||
return $redis;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @requires extension redis
|
||||
*/
|
||||
class RedisArrayStoreTest extends AbstractRedisStoreTest
|
||||
{
|
||||
public static function setupBeforeClass()
|
||||
{
|
||||
if (!class_exists('RedisArray')) {
|
||||
self::markTestSkipped('The RedisArray class is required.');
|
||||
}
|
||||
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
|
||||
$e = error_get_last();
|
||||
self::markTestSkipped($e['message']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRedisConnection()
|
||||
{
|
||||
$redis = new \RedisArray(array(getenv('REDIS_HOST')));
|
||||
|
||||
return $redis;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @requires extension redis
|
||||
*/
|
||||
class RedisStoreTest extends AbstractRedisStoreTest
|
||||
{
|
||||
public static function setupBeforeClass()
|
||||
{
|
||||
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
|
||||
$e = error_get_last();
|
||||
self::markTestSkipped($e['message']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRedisConnection()
|
||||
{
|
||||
$redis = new \Redis();
|
||||
$redis->connect(getenv('REDIS_HOST'));
|
||||
|
||||
return $redis;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Store\RedisStore;
|
||||
use Symfony\Component\Lock\Store\RetryTillSaveStore;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class RetryTillSaveStoreTest extends AbstractStoreTest
|
||||
{
|
||||
use BlockingStoreTestTrait;
|
||||
|
||||
public function getStore()
|
||||
{
|
||||
$redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
|
||||
try {
|
||||
$redis->connect();
|
||||
} catch (\Exception $e) {
|
||||
self::markTestSkipped($e->getMessage());
|
||||
}
|
||||
|
||||
return new RetryTillSaveStore(new RedisStore($redis), 100, 100);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?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\Tests\Store;
|
||||
|
||||
use Symfony\Component\Lock\Store\SemaphoreStore;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*
|
||||
* @requires extension sysvsem
|
||||
*/
|
||||
class SemaphoreStoreTest extends AbstractStoreTest
|
||||
{
|
||||
use BlockingStoreTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getStore()
|
||||
{
|
||||
return new SemaphoreStore();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
<?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\Tests\Strategy;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Lock\Strategy\ConsensusStrategy;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class ConsensusStrategyTest extends TestCase
|
||||
{
|
||||
/** @var ConsensusStrategy */
|
||||
private $strategy;
|
||||
|
||||
public function setup()
|
||||
{
|
||||
$this->strategy = new ConsensusStrategy();
|
||||
}
|
||||
|
||||
public function provideMetResults()
|
||||
{
|
||||
// success, failure, total, isMet
|
||||
yield array(3, 0, 3, true);
|
||||
yield array(2, 1, 3, true);
|
||||
yield array(2, 0, 3, true);
|
||||
yield array(1, 2, 3, false);
|
||||
yield array(1, 1, 3, false);
|
||||
yield array(1, 0, 3, false);
|
||||
yield array(0, 3, 3, false);
|
||||
yield array(0, 2, 3, false);
|
||||
yield array(0, 1, 3, false);
|
||||
yield array(0, 0, 3, false);
|
||||
|
||||
yield array(2, 0, 2, true);
|
||||
yield array(1, 1, 2, false);
|
||||
yield array(1, 0, 2, false);
|
||||
yield array(0, 2, 2, false);
|
||||
yield array(0, 1, 2, false);
|
||||
yield array(0, 0, 2, false);
|
||||
}
|
||||
|
||||
public function provideIndeterminate()
|
||||
{
|
||||
// success, failure, total, canBeMet
|
||||
yield array(3, 0, 3, true);
|
||||
yield array(2, 1, 3, true);
|
||||
yield array(2, 0, 3, true);
|
||||
yield array(1, 2, 3, false);
|
||||
yield array(1, 1, 3, true);
|
||||
yield array(1, 0, 3, true);
|
||||
yield array(0, 3, 3, false);
|
||||
yield array(0, 2, 3, false);
|
||||
yield array(0, 1, 3, true);
|
||||
yield array(0, 0, 3, true);
|
||||
|
||||
yield array(2, 0, 2, true);
|
||||
yield array(1, 1, 2, false);
|
||||
yield array(1, 0, 2, true);
|
||||
yield array(0, 2, 2, false);
|
||||
yield array(0, 1, 2, false);
|
||||
yield array(0, 0, 2, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMetResults
|
||||
*/
|
||||
public function testMet($success, $failure, $total, $isMet)
|
||||
{
|
||||
$this->assertSame($isMet, $this->strategy->isMet($success, $total));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIndeterminate
|
||||
*/
|
||||
public function canBeMet($success, $failure, $total, $isMet)
|
||||
{
|
||||
$this->assertSame($isMet, $this->strategy->canBeMet($failure, $total));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
<?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\Tests\Strategy;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Lock\Strategy\UnanimousStrategy;
|
||||
|
||||
/**
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class UnanimousStrategyTest extends TestCase
|
||||
{
|
||||
/** @var UnanimousStrategy */
|
||||
private $strategy;
|
||||
|
||||
public function setup()
|
||||
{
|
||||
$this->strategy = new UnanimousStrategy();
|
||||
}
|
||||
|
||||
public function provideMetResults()
|
||||
{
|
||||
// success, failure, total, isMet
|
||||
yield array(3, 0, 3, true);
|
||||
yield array(2, 1, 3, false);
|
||||
yield array(2, 0, 3, false);
|
||||
yield array(1, 2, 3, false);
|
||||
yield array(1, 1, 3, false);
|
||||
yield array(1, 0, 3, false);
|
||||
yield array(0, 3, 3, false);
|
||||
yield array(0, 2, 3, false);
|
||||
yield array(0, 1, 3, false);
|
||||
yield array(0, 0, 3, false);
|
||||
|
||||
yield array(2, 0, 2, true);
|
||||
yield array(1, 1, 2, false);
|
||||
yield array(1, 0, 2, false);
|
||||
yield array(0, 2, 2, false);
|
||||
yield array(0, 1, 2, false);
|
||||
yield array(0, 0, 2, false);
|
||||
}
|
||||
|
||||
public function provideIndeterminate()
|
||||
{
|
||||
// success, failure, total, canBeMet
|
||||
yield array(3, 0, 3, true);
|
||||
yield array(2, 1, 3, false);
|
||||
yield array(2, 0, 3, true);
|
||||
yield array(1, 2, 3, false);
|
||||
yield array(1, 1, 3, false);
|
||||
yield array(1, 0, 3, true);
|
||||
yield array(0, 3, 3, false);
|
||||
yield array(0, 2, 3, false);
|
||||
yield array(0, 1, 3, false);
|
||||
yield array(0, 0, 3, true);
|
||||
|
||||
yield array(2, 0, 2, true);
|
||||
yield array(1, 1, 2, false);
|
||||
yield array(1, 0, 2, true);
|
||||
yield array(0, 2, 2, false);
|
||||
yield array(0, 1, 2, false);
|
||||
yield array(0, 0, 2, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMetResults
|
||||
*/
|
||||
public function testMet($success, $failure, $total, $isMet)
|
||||
{
|
||||
$this->assertSame($isMet, $this->strategy->isMet($success, $total));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideIndeterminate
|
||||
*/
|
||||
public function canBeMet($success, $failure, $total, $isMet)
|
||||
{
|
||||
$this->assertSame($isMet, $this->strategy->canBeMet($failure, $total));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"name": "symfony/lock",
|
||||
"type": "library",
|
||||
"description": "Symfony Lock Component",
|
||||
"keywords": ["locking", "redlock", "mutex", "semaphore", "flock", "cas"],
|
||||
"homepage": "https://symfony.com",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jérémy Derussé",
|
||||
"email": "jeremy@derusse.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.1.3",
|
||||
"symfony/polyfill-php70": "~1.0",
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"predis/predis": "~1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Symfony\\Component\\Lock\\": "" },
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.0-dev"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
bootstrap="vendor/autoload.php"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<env name="REDIS_HOST" value="localhost" />
|
||||
<env name="MEMCACHED_HOST" value="localhost" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Symfony Lock Component Test Suite">
|
||||
<directory>./Tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory>./</directory>
|
||||
<exclude>
|
||||
<directory>./Tests</directory>
|
||||
<directory>./vendor</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
|
@ -320,7 +320,7 @@ class Process implements \IteratorAggregate
|
|||
}
|
||||
if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
|
||||
$this->options['bypass_shell'] = true;
|
||||
$commandline = $this->prepareWindowsCommandLine($commandline, $envBackup);
|
||||
$commandline = $this->prepareWindowsCommandLine($commandline, $envBackup, $env);
|
||||
} elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
|
||||
// last exit code is output on the fourth pipe and caught to work around --enable-sigchild
|
||||
$descriptors[3] = array('pipe', 'w');
|
||||
|
@ -1627,7 +1627,7 @@ class Process implements \IteratorAggregate
|
|||
return true;
|
||||
}
|
||||
|
||||
private function prepareWindowsCommandLine($cmd, array &$envBackup)
|
||||
private function prepareWindowsCommandLine($cmd, array &$envBackup, array &$env = null)
|
||||
{
|
||||
$uid = uniqid('', true);
|
||||
$varCount = 0;
|
||||
|
@ -1640,7 +1640,7 @@ class Process implements \IteratorAggregate
|
|||
[^"%!^]*+
|
||||
)++
|
||||
)"/x',
|
||||
function ($m) use (&$envBackup, &$varCache, &$varCount, $uid) {
|
||||
function ($m) use (&$envBackup, &$env, &$varCache, &$varCount, $uid) {
|
||||
if (isset($varCache[$m[0]])) {
|
||||
return $varCache[$m[0]];
|
||||
}
|
||||
|
@ -1652,10 +1652,15 @@ class Process implements \IteratorAggregate
|
|||
}
|
||||
|
||||
$value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value);
|
||||
$value = preg_replace('/(\\\\*)"/', '$1$1\\"', $value);
|
||||
|
||||
$value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
|
||||
$var = $uid.++$varCount;
|
||||
putenv("$var=\"$value\"");
|
||||
|
||||
if (null === $env) {
|
||||
putenv("$var=$value");
|
||||
} else {
|
||||
$env[$var] = $value;
|
||||
}
|
||||
|
||||
$envBackup[$var] = false;
|
||||
|
||||
return $varCache[$m[0]] = '!'.$var.'!';
|
||||
|
|
|
@ -1465,6 +1465,19 @@ class ProcessTest extends TestCase
|
|||
$this->assertSame($arg, $p->getOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideEscapeArgument
|
||||
* @group legacy
|
||||
*/
|
||||
public function testEscapeArgumentWhenInheritEnvDisabled($arg)
|
||||
{
|
||||
$p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg), null, array('BAR' => 'BAZ'));
|
||||
$p->inheritEnvironmentVariables(false);
|
||||
$p->run();
|
||||
|
||||
$this->assertSame($arg, $p->getOutput());
|
||||
}
|
||||
|
||||
public function provideEscapeArgument()
|
||||
{
|
||||
yield array('a"b%c%');
|
||||
|
|
|
@ -11,14 +11,5 @@
|
|||
<condition>context.getMethod() == "GET"</condition>
|
||||
</route>
|
||||
|
||||
<route id="blog_show_legacy" path="/blog/{slug}" host="{locale}.example.com">
|
||||
<default key="_controller">MyBundle:Blog:show</default>
|
||||
<requirement key="_method">GET|POST|put|OpTiOnS</requirement>
|
||||
<requirement key="_scheme">hTTps</requirement>
|
||||
<requirement key="locale">\w+</requirement>
|
||||
<option key="compiler_class">RouteCompiler</option>
|
||||
<condition>context.getMethod() == "GET"</condition>
|
||||
</route>
|
||||
|
||||
<route id="blog_show_inherited" path="/blog/{slug}" />
|
||||
</routes>
|
||||
|
|
|
@ -70,7 +70,7 @@ class XmlFileLoaderTest extends TestCase
|
|||
$routeCollection = $loader->load('validresource.xml');
|
||||
$routes = $routeCollection->all();
|
||||
|
||||
$this->assertCount(3, $routes, 'Two routes are loaded');
|
||||
$this->assertCount(2, $routes, 'Two routes are loaded');
|
||||
$this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
|
||||
|
||||
foreach ($routes as $route) {
|
||||
|
|
|
@ -655,17 +655,17 @@ pre.sf-dump code {
|
|||
border: 1px solid #ffa500;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.sf-dump-search-hidden {
|
||||
pre.sf-dump .sf-dump-search-hidden {
|
||||
display: none;
|
||||
}
|
||||
.sf-dump-search-wrapper {
|
||||
pre.sf-dump .sf-dump-search-wrapper {
|
||||
float: right;
|
||||
font-size: 0;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
text-align: right;
|
||||
}
|
||||
.sf-dump-search-wrapper > * {
|
||||
pre.sf-dump .sf-dump-search-wrapper > * {
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
height: 21px;
|
||||
|
@ -675,7 +675,7 @@ pre.sf-dump code {
|
|||
color: #757575;
|
||||
border: 1px solid #BBB;
|
||||
}
|
||||
.sf-dump-search-wrapper > input.sf-dump-search-input {
|
||||
pre.sf-dump .sf-dump-search-wrapper > input.sf-dump-search-input {
|
||||
padding: 3px;
|
||||
height: 21px;
|
||||
font-size: 12px;
|
||||
|
@ -685,25 +685,25 @@ pre.sf-dump code {
|
|||
border-bottom-left-radius: 3px;
|
||||
color: #000;
|
||||
}
|
||||
.sf-dump-search-wrapper > .sf-dump-search-input-next,
|
||||
.sf-dump-search-wrapper > .sf-dump-search-input-previous {
|
||||
pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next,
|
||||
pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous {
|
||||
background: #F2F2F2;
|
||||
outline: none;
|
||||
border-left: none;
|
||||
font-size: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
.sf-dump-search-wrapper > .sf-dump-search-input-next {
|
||||
pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
.sf-dump-search-wrapper > .sf-dump-search-input-next > svg,
|
||||
.sf-dump-search-wrapper > .sf-dump-search-input-previous > svg {
|
||||
pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next > svg,
|
||||
pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous > svg {
|
||||
pointer-events: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.sf-dump-search-wrapper > .sf-dump-search-count {
|
||||
pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-count {
|
||||
display: inline-block;
|
||||
padding: 0 5px;
|
||||
margin: 0;
|
||||
|
|
|
@ -360,7 +360,11 @@ class Parser
|
|||
|
||||
foreach ($this->lines as $line) {
|
||||
try {
|
||||
$parsedLine = Inline::parse($line, $flags, $this->refs);
|
||||
if (isset($line[0]) && ('"' === $line[0] || "'" === $line[0])) {
|
||||
$parsedLine = $line;
|
||||
} else {
|
||||
$parsedLine = Inline::parse($line, $flags, $this->refs);
|
||||
}
|
||||
|
||||
if (!is_string($parsedLine)) {
|
||||
$parseError = true;
|
||||
|
|
|
@ -1561,8 +1561,18 @@ EOT;
|
|||
$this->assertEquals("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz"));
|
||||
}
|
||||
|
||||
public function testParseMultiLineMappingValue()
|
||||
/**
|
||||
* @dataProvider multiLineDataProvider
|
||||
*/
|
||||
public function testParseMultiLineMappingValue($yaml, $expected, $parseError)
|
||||
{
|
||||
$this->assertEquals($expected, $this->parser->parse($yaml));
|
||||
}
|
||||
|
||||
public function multiLineDataProvider()
|
||||
{
|
||||
$tests = array();
|
||||
|
||||
$yaml = <<<'EOF'
|
||||
foo:
|
||||
- bar:
|
||||
|
@ -1579,7 +1589,43 @@ EOF;
|
|||
),
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, $this->parser->parse($yaml));
|
||||
$tests[] = array($yaml, $expected, false);
|
||||
|
||||
$yaml = <<<'EOF'
|
||||
bar
|
||||
"foo"
|
||||
EOF;
|
||||
$expected = 'bar "foo"';
|
||||
|
||||
$tests[] = array($yaml, $expected, false);
|
||||
|
||||
$yaml = <<<'EOF'
|
||||
bar
|
||||
"foo
|
||||
EOF;
|
||||
$expected = 'bar "foo';
|
||||
|
||||
$tests[] = array($yaml, $expected, false);
|
||||
|
||||
$yaml = <<<'EOF'
|
||||
bar
|
||||
|
||||
'foo'
|
||||
EOF;
|
||||
$expected = "bar\n'foo'";
|
||||
|
||||
$tests[] = array($yaml, $expected, false);
|
||||
|
||||
$yaml = <<<'EOF'
|
||||
bar
|
||||
|
||||
foo'
|
||||
EOF;
|
||||
$expected = "bar\nfoo'";
|
||||
|
||||
$tests[] = array($yaml, $expected, false);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
public function testTaggedInlineMapping()
|
||||
|
|
Reference in New Issue