From 8dbd8505ed9f8e942d868e817d1bba1bb47316b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Sat, 20 Nov 2021 15:19:12 +0100 Subject: [PATCH] [FrameworkBundle] Add semaphore configuration --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 57 +++++++++++++++++++ .../FrameworkExtension.php | 41 +++++++++++++ .../Resources/config/schema/symfony-1.0.xsd | 16 ++++++ .../Resources/config/semaphore.php | 23 ++++++++ .../DependencyInjection/ConfigurationTest.php | 56 ++++++++++++++++++ .../Fixtures/xml/semaphore.xml | 11 ++++ .../Fixtures/yml/semaphore.yml | 2 + .../Fixtures/yml/semaphore_named.yml | 7 +++ .../Bundle/FrameworkBundle/composer.json | 1 + 10 files changed, 215 insertions(+) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Resources/config/semaphore.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/semaphore.xml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore.yml create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/semaphore_named.yml 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",