diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index a5804bcb15913..496072329f47b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
6.1
---
+ * Add support for configuring semaphores
* Environment variable `SYMFONY_IDE` is read by default when `framework.ide` config is not set.
* Load PHP configuration files by default in the `MicroKernelTrait`
* Add `cache:pool:invalidate-tags` command
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 953fa3db97806..6634a521b155a 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -35,6 +35,7 @@
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
+use Symfony\Component\Semaphore\Semaphore;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Uid\Factory\UuidFactory;
@@ -153,6 +154,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$this->addExceptionsSection($rootNode);
$this->addWebLinkSection($rootNode, $enableIfStandalone);
$this->addLockSection($rootNode, $enableIfStandalone);
+ $this->addSemaphoreSection($rootNode, $enableIfStandalone);
$this->addMessengerSection($rootNode, $enableIfStandalone);
$this->addRobotsIndexSection($rootNode);
$this->addHttpClientSection($rootNode, $enableIfStandalone);
@@ -1278,6 +1280,61 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI
;
}
+ private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
+ {
+ $rootNode
+ ->children()
+ ->arrayNode('semaphore')
+ ->info('Semaphore configuration')
+ ->{$enableIfStandalone('symfony/semaphore', Semaphore::class)}()
+ ->beforeNormalization()
+ ->ifString()->then(function ($v) { return ['enabled' => true, 'resources' => $v]; })
+ ->end()
+ ->beforeNormalization()
+ ->ifTrue(function ($v) { return \is_array($v) && !isset($v['enabled']); })
+ ->then(function ($v) { return $v + ['enabled' => true]; })
+ ->end()
+ ->beforeNormalization()
+ ->ifTrue(function ($v) { return \is_array($v) && !isset($v['resources']) && !isset($v['resource']); })
+ ->then(function ($v) {
+ $e = $v['enabled'];
+ unset($v['enabled']);
+
+ return ['enabled' => $e, 'resources' => $v];
+ })
+ ->end()
+ ->addDefaultsIfNotSet()
+ ->fixXmlConfig('resource')
+ ->children()
+ ->arrayNode('resources')
+ ->normalizeKeys(false)
+ ->useAttributeAsKey('name')
+ ->requiresAtLeastOneElement()
+ ->beforeNormalization()
+ ->ifString()->then(function ($v) { return ['default' => $v]; })
+ ->end()
+ ->beforeNormalization()
+ ->ifTrue(function ($v) { return \is_array($v) && array_is_list($v); })
+ ->then(function ($v) {
+ $resources = [];
+ foreach ($v as $resource) {
+ $resources[] = \is_array($resource) && isset($resource['name'])
+ ? [$resource['name'] => $resource['value']]
+ : ['default' => $resource]
+ ;
+ }
+
+ return array_merge_recursive([], ...$resources);
+ })
+ ->end()
+ ->prototype('scalar')->end()
+ ->end()
+ ->end()
+ ->end()
+ ->end()
+ ;
+ }
+
private function addWebLinkSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 5296bda25513e..febcf2a263090 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -185,6 +185,10 @@
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
+use Symfony\Component\Semaphore\PersistingStoreInterface as SemaphoreStoreInterface;
+use Symfony\Component\Semaphore\Semaphore;
+use Symfony\Component\Semaphore\SemaphoreFactory;
+use Symfony\Component\Semaphore\Store\StoreFactory as SemaphoreStoreFactory;
use Symfony\Component\Serializer\Encoder\DecoderInterface;
use Symfony\Component\Serializer\Encoder\EncoderInterface;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@@ -402,6 +406,10 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerLockConfiguration($config['lock'], $container, $loader);
}
+ if ($this->isConfigEnabled($container, $config['semaphore'])) {
+ $this->registerSemaphoreConfiguration($config['semaphore'], $container, $loader);
+ }
+
if ($this->isConfigEnabled($container, $config['rate_limiter'])) {
if (!interface_exists(LimiterInterface::class)) {
throw new LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
@@ -1890,6 +1898,39 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont
}
}
+ private function registerSemaphoreConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
+ {
+ $loader->load('semaphore.php');
+
+ foreach ($config['resources'] as $resourceName => $resourceStore) {
+ $storeDsn = $container->resolveEnvPlaceholders($resourceStore, null, $usedEnvs);
+ $storeDefinition = new Definition(SemaphoreStoreInterface::class);
+ $storeDefinition->setFactory([SemaphoreStoreFactory::class, 'createStore']);
+ $storeDefinition->setArguments([$resourceStore]);
+
+ $container->setDefinition($storeDefinitionId = '.semaphore.'.$resourceName.'.store.'.$container->hash($storeDsn), $storeDefinition);
+
+ // Generate factories for each resource
+ $factoryDefinition = new ChildDefinition('semaphore.factory.abstract');
+ $factoryDefinition->replaceArgument(0, new Reference($storeDefinitionId));
+ $container->setDefinition('semaphore.'.$resourceName.'.factory', $factoryDefinition);
+
+ // Generate services for semaphore instances
+ $semaphoreDefinition = new Definition(Semaphore::class);
+ $semaphoreDefinition->setPublic(false);
+ $semaphoreDefinition->setFactory([new Reference('semaphore.'.$resourceName.'.factory'), 'createSemaphore']);
+ $semaphoreDefinition->setArguments([$resourceName]);
+
+ // provide alias for default resource
+ if ('default' === $resourceName) {
+ $container->setAlias('semaphore.factory', new Alias('semaphore.'.$resourceName.'.factory', false));
+ $container->setAlias(SemaphoreFactory::class, new Alias('semaphore.factory', false));
+ } else {
+ $container->registerAliasForArgument('semaphore.'.$resourceName.'.factory', SemaphoreFactory::class, $resourceName.'.semaphore.factory');
+ }
+ }
+ }
+
private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $validationConfig)
{
if (!interface_exists(MessageBusInterface::class)) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
index d48e8ca1520f7..5e3de4a9c9b8b 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
@@ -31,6 +31,7 @@
+
@@ -481,6 +482,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/semaphore.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/semaphore.php
new file mode 100644
index 0000000000000..ce35c25089548
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/semaphore.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DependencyInjection\Loader\Configurator;
+
+use Symfony\Component\Semaphore\SemaphoreFactory;
+
+return static function (ContainerConfigurator $container) {
+ $container->services()
+ ->set('semaphore.factory.abstract', SemaphoreFactory::class)->abstract()
+ ->args([abstract_arg('Store')])
+ ->call('setLogger', [service('logger')->ignoreOnInvalid()])
+ ->tag('monolog.logger', ['channel' => 'semaphore'])
+ ;
+};
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
index 1b154cb76f2a9..67a2a1fa90977 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
@@ -265,6 +265,57 @@ public function testLockMergeConfigs()
);
}
+ /**
+ * @dataProvider provideValidSemaphoreConfigurationTests
+ */
+ public function testValidSemaphoreConfiguration($semaphoreConfig, $processedConfig)
+ {
+ $processor = new Processor();
+ $configuration = new Configuration(true);
+ $config = $processor->processConfiguration($configuration, [
+ [
+ 'semaphore' => $semaphoreConfig,
+ ],
+ ]);
+
+ $this->assertArrayHasKey('semaphore', $config);
+
+ $this->assertEquals($processedConfig, $config['semaphore']);
+ }
+
+ public function provideValidSemaphoreConfigurationTests()
+ {
+ yield [null, ['enabled' => true, 'resources' => []]];
+
+ yield ['redis://default', ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
+ yield [['foo' => 'redis://foo', 'bar' => 'redis://bar'], ['enabled' => true, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
+ yield [['default' => 'redis://default'], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
+
+ yield [['enabled' => false, 'redis://default'], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
+ yield [['enabled' => false, 'foo' => 'redis://foo', 'bar' => 'redis://bar'], ['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
+ yield [['enabled' => false, 'default' => 'redis://default'], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
+
+ yield [['resources' => 'redis://default'], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
+ yield [['resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']], ['enabled' => true, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
+ yield [['resources' => ['default' => 'redis://default']], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
+
+ yield [['enabled' => false, 'resources' => 'redis://default'], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
+ yield [['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']], ['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
+ yield [['enabled' => false, 'resources' => ['default' => 'redis://default']], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
+
+ // xml
+
+ yield [['resource' => ['redis://default']], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
+ yield [['resource' => ['redis://default', ['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => true, 'resources' => ['default' => 'redis://default', 'foo' => 'redis://default']]];
+ yield [['resource' => [['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => true, 'resources' => ['foo' => 'redis://default']]];
+ yield [['resource' => [['name' => 'foo', 'value' => 'redis://default'], ['name' => 'bar', 'value' => 'redis://default']]], ['enabled' => true, 'resources' => ['foo' => 'redis://default', 'bar' => 'redis://default']]];
+
+ yield [['enabled' => false, 'resource' => ['redis://default']], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
+ yield [['enabled' => false, 'resource' => ['redis://default', ['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => false, 'resources' => ['default' => 'redis://default', 'foo' => 'redis://default']]];
+ yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => false, 'resources' => ['foo' => 'redis://default']]];
+ yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'redis://foo'], ['name' => 'bar', 'value' => 'redis://bar']]], ['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
+ }
+
public function testItShowANiceMessageIfTwoMessengerBusesAreConfiguredButNoDefaultBus()
{
$expectedMessage = 'You must specify the "default_bus" if you define more than one bus.';
@@ -524,6 +575,11 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
],
],
],
+ 'semaphore' => [
+ 'enabled' => !class_exists(FullStack::class),
+ 'resources' => [
+ ],
+ ],
'messenger' => [
'enabled' => !class_exists(FullStack::class) && interface_exists(MessageBusInterface::class),
'routing' => [],
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/semaphore.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/semaphore.xml
new file mode 100644
index 0000000000000..0b2f6c662dcf5
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/semaphore.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore.yml
new file mode 100644
index 0000000000000..47b1323517b4c
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore.yml
@@ -0,0 +1,2 @@
+framework:
+ semaphore: redis://localhost
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore_named.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore_named.yml
new file mode 100644
index 0000000000000..0a29e4ea825e2
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore_named.yml
@@ -0,0 +1,7 @@
+parameters:
+ env(REDIS_DSN): redis://paas.com
+
+framework:
+ semaphore:
+ foo: redis://paas.com
+ qux: "%env(REDIS_DSN)%"
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 08aeb65863919..89957f7b2a64a 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -53,6 +53,7 @@
"symfony/process": "^5.4|^6.0",
"symfony/rate-limiter": "^5.4|^6.0",
"symfony/security-bundle": "^5.4|^6.0",
+ "symfony/semaphore": "^5.4|^6.0",
"symfony/serializer": "^5.4|^6.0",
"symfony/stopwatch": "^5.4|^6.0",
"symfony/string": "^5.4|^6.0",