Skip to content

Commit 1fdaaea

Browse files
committed
[FrameworkBundle] Add semaphore configuration
1 parent aa97779 commit 1fdaaea

File tree

10 files changed

+215
-0
lines changed

10 files changed

+215
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
6.1
55
---
66

7+
* Add support for configuring semaphores
78
* Environment variable `SYMFONY_IDE` is read by default when `framework.ide` config is not set.
89
* Load PHP configuration files by default in the `MicroKernelTrait`
910
* Add `cache:pool:invalidate-tags` command

DependencyInjection/Configuration.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Symfony\Component\PropertyAccess\PropertyAccessor;
3636
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
3737
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
38+
use Symfony\Component\Semaphore\Semaphore;
3839
use Symfony\Component\Serializer\Serializer;
3940
use Symfony\Component\Translation\Translator;
4041
use Symfony\Component\Uid\Factory\UuidFactory;
@@ -153,6 +154,7 @@ public function getConfigTreeBuilder(): TreeBuilder
153154
$this->addExceptionsSection($rootNode);
154155
$this->addWebLinkSection($rootNode, $enableIfStandalone);
155156
$this->addLockSection($rootNode, $enableIfStandalone);
157+
$this->addSemaphoreSection($rootNode, $enableIfStandalone);
156158
$this->addMessengerSection($rootNode, $enableIfStandalone);
157159
$this->addRobotsIndexSection($rootNode);
158160
$this->addHttpClientSection($rootNode, $enableIfStandalone);
@@ -1278,6 +1280,61 @@ private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableI
12781280
;
12791281
}
12801282

1283+
private function addSemaphoreSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
1284+
{
1285+
$rootNode
1286+
->children()
1287+
->arrayNode('semaphore')
1288+
->info('Semaphore configuration')
1289+
->{$enableIfStandalone('symfony/semaphore', Semaphore::class)}()
1290+
->beforeNormalization()
1291+
->ifString()->then(function ($v) { return ['enabled' => true, 'resources' => $v]; })
1292+
->end()
1293+
->beforeNormalization()
1294+
->ifTrue(function ($v) { return \is_array($v) && !isset($v['enabled']); })
1295+
->then(function ($v) { return $v + ['enabled' => true]; })
1296+
->end()
1297+
->beforeNormalization()
1298+
->ifTrue(function ($v) { return \is_array($v) && !isset($v['resources']) && !isset($v['resource']); })
1299+
->then(function ($v) {
1300+
$e = $v['enabled'];
1301+
unset($v['enabled']);
1302+
1303+
return ['enabled' => $e, 'resources' => $v];
1304+
})
1305+
->end()
1306+
->addDefaultsIfNotSet()
1307+
->fixXmlConfig('resource')
1308+
->children()
1309+
->arrayNode('resources')
1310+
->normalizeKeys(false)
1311+
->useAttributeAsKey('name')
1312+
->requiresAtLeastOneElement()
1313+
->beforeNormalization()
1314+
->ifString()->then(function ($v) { return ['default' => $v]; })
1315+
->end()
1316+
->beforeNormalization()
1317+
->ifTrue(function ($v) { return \is_array($v) && array_is_list($v); })
1318+
->then(function ($v) {
1319+
$resources = [];
1320+
foreach ($v as $resource) {
1321+
$resources[] = \is_array($resource) && isset($resource['name'])
1322+
? [$resource['name'] => $resource['value']]
1323+
: ['default' => $resource]
1324+
;
1325+
}
1326+
1327+
return array_merge_recursive([], ...$resources);
1328+
})
1329+
->end()
1330+
->prototype('scalar')->end()
1331+
->end()
1332+
->end()
1333+
->end()
1334+
->end()
1335+
;
1336+
}
1337+
12811338
private function addWebLinkSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
12821339
{
12831340
$rootNode

DependencyInjection/FrameworkExtension.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@
185185
use Symfony\Component\Security\Core\Exception\AuthenticationException;
186186
use Symfony\Component\Security\Core\Security;
187187
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
188+
use Symfony\Component\Semaphore\PersistingStoreInterface as SemaphoreStoreInterface;
189+
use Symfony\Component\Semaphore\Semaphore;
190+
use Symfony\Component\Semaphore\SemaphoreFactory;
191+
use Symfony\Component\Semaphore\Store\StoreFactory as SemaphoreStoreFactory;
188192
use Symfony\Component\Serializer\Encoder\DecoderInterface;
189193
use Symfony\Component\Serializer\Encoder\EncoderInterface;
190194
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
@@ -402,6 +406,10 @@ public function load(array $configs, ContainerBuilder $container)
402406
$this->registerLockConfiguration($config['lock'], $container, $loader);
403407
}
404408

409+
if ($this->isConfigEnabled($container, $config['semaphore'])) {
410+
$this->registerSemaphoreConfiguration($config['semaphore'], $container, $loader);
411+
}
412+
405413
if ($this->isConfigEnabled($container, $config['rate_limiter'])) {
406414
if (!interface_exists(LimiterInterface::class)) {
407415
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
18901898
}
18911899
}
18921900

1901+
private function registerSemaphoreConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
1902+
{
1903+
$loader->load('semaphore.php');
1904+
1905+
foreach ($config['resources'] as $resourceName => $resourceStore) {
1906+
$storeDsn = $container->resolveEnvPlaceholders($resourceStore, null, $usedEnvs);
1907+
$storeDefinition = new Definition(SemaphoreStoreInterface::class);
1908+
$storeDefinition->setFactory([SemaphoreStoreFactory::class, 'createStore']);
1909+
$storeDefinition->setArguments([$resourceStore]);
1910+
1911+
$container->setDefinition($storeDefinitionId = '.semaphore.'.$resourceName.'.store.'.$container->hash($storeDsn), $storeDefinition);
1912+
1913+
// Generate factories for each resource
1914+
$factoryDefinition = new ChildDefinition('semaphore.factory.abstract');
1915+
$factoryDefinition->replaceArgument(0, new Reference($storeDefinitionId));
1916+
$container->setDefinition('semaphore.'.$resourceName.'.factory', $factoryDefinition);
1917+
1918+
// Generate services for semaphore instances
1919+
$semaphoreDefinition = new Definition(Semaphore::class);
1920+
$semaphoreDefinition->setPublic(false);
1921+
$semaphoreDefinition->setFactory([new Reference('semaphore.'.$resourceName.'.factory'), 'createSemaphore']);
1922+
$semaphoreDefinition->setArguments([$resourceName]);
1923+
1924+
// provide alias for default resource
1925+
if ('default' === $resourceName) {
1926+
$container->setAlias('semaphore.factory', new Alias('semaphore.'.$resourceName.'.factory', false));
1927+
$container->setAlias(SemaphoreFactory::class, new Alias('semaphore.factory', false));
1928+
} else {
1929+
$container->registerAliasForArgument('semaphore.'.$resourceName.'.factory', SemaphoreFactory::class, $resourceName.'.semaphore.factory');
1930+
}
1931+
}
1932+
}
1933+
18931934
private function registerMessengerConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader, array $validationConfig)
18941935
{
18951936
if (!interface_exists(MessageBusInterface::class)) {

Resources/config/schema/symfony-1.0.xsd

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<xsd:element name="php-errors" type="php-errors" minOccurs="0" maxOccurs="1" />
3232
<xsd:element name="exceptions" type="exceptions" minOccurs="0" maxOccurs="1" />
3333
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
34+
<xsd:element name="semaphore" type="semaphore" minOccurs="0" maxOccurs="1" />
3435
<xsd:element name="messenger" type="messenger" minOccurs="0" maxOccurs="1" />
3536
<xsd:element name="http-client" type="http_client" minOccurs="0" maxOccurs="1" />
3637
<xsd:element name="mailer" type="mailer" minOccurs="0" maxOccurs="1" />
@@ -481,6 +482,21 @@
481482
</xsd:simpleContent>
482483
</xsd:complexType>
483484

485+
<xsd:complexType name="semaphore">
486+
<xsd:sequence>
487+
<xsd:element name="resource" type="semaphore_resource" minOccurs="1" maxOccurs="unbounded" />
488+
</xsd:sequence>
489+
<xsd:attribute name="enabled" type="xsd:boolean" />
490+
</xsd:complexType>
491+
492+
<xsd:complexType name="semaphore_resource">
493+
<xsd:simpleContent>
494+
<xsd:extension base="xsd:string">
495+
<xsd:attribute name="name" type="xsd:string" />
496+
</xsd:extension>
497+
</xsd:simpleContent>
498+
</xsd:complexType>
499+
484500
<xsd:complexType name="messenger">
485501
<xsd:sequence>
486502
<xsd:element name="serializer" type="messenger_serializer" minOccurs="0" />

Resources/config/semaphore.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\Semaphore\SemaphoreFactory;
15+
16+
return static function (ContainerConfigurator $container) {
17+
$container->services()
18+
->set('semaphore.factory.abstract', SemaphoreFactory::class)->abstract()
19+
->args([abstract_arg('Store')])
20+
->call('setLogger', [service('logger')->ignoreOnInvalid()])
21+
->tag('monolog.logger', ['channel' => 'semaphore'])
22+
;
23+
};

Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,57 @@ public function testLockMergeConfigs()
265265
);
266266
}
267267

268+
/**
269+
* @dataProvider provideValidSemaphoreConfigurationTests
270+
*/
271+
public function testValidSemaphoreConfiguration($semaphoreConfig, $processedConfig)
272+
{
273+
$processor = new Processor();
274+
$configuration = new Configuration(true);
275+
$config = $processor->processConfiguration($configuration, [
276+
[
277+
'semaphore' => $semaphoreConfig,
278+
],
279+
]);
280+
281+
$this->assertArrayHasKey('semaphore', $config);
282+
283+
$this->assertEquals($processedConfig, $config['semaphore']);
284+
}
285+
286+
public function provideValidSemaphoreConfigurationTests()
287+
{
288+
yield [null, ['enabled' => true, 'resources' => []]];
289+
290+
yield ['redis://default', ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
291+
yield [['foo' => 'redis://foo', 'bar' => 'redis://bar'], ['enabled' => true, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
292+
yield [['default' => 'redis://default'], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
293+
294+
yield [['enabled' => false, 'redis://default'], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
295+
yield [['enabled' => false, 'foo' => 'redis://foo', 'bar' => 'redis://bar'], ['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
296+
yield [['enabled' => false, 'default' => 'redis://default'], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
297+
298+
yield [['resources' => 'redis://default'], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
299+
yield [['resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']], ['enabled' => true, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
300+
yield [['resources' => ['default' => 'redis://default']], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
301+
302+
yield [['enabled' => false, 'resources' => 'redis://default'], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
303+
yield [['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']], ['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
304+
yield [['enabled' => false, 'resources' => ['default' => 'redis://default']], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
305+
306+
// xml
307+
308+
yield [['resource' => ['redis://default']], ['enabled' => true, 'resources' => ['default' => 'redis://default']]];
309+
yield [['resource' => ['redis://default', ['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => true, 'resources' => ['default' => 'redis://default', 'foo' => 'redis://default']]];
310+
yield [['resource' => [['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => true, 'resources' => ['foo' => 'redis://default']]];
311+
yield [['resource' => [['name' => 'foo', 'value' => 'redis://default'], ['name' => 'bar', 'value' => 'redis://default']]], ['enabled' => true, 'resources' => ['foo' => 'redis://default', 'bar' => 'redis://default']]];
312+
313+
yield [['enabled' => false, 'resource' => ['redis://default']], ['enabled' => false, 'resources' => ['default' => 'redis://default']]];
314+
yield [['enabled' => false, 'resource' => ['redis://default', ['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => false, 'resources' => ['default' => 'redis://default', 'foo' => 'redis://default']]];
315+
yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'redis://default']]], ['enabled' => false, 'resources' => ['foo' => 'redis://default']]];
316+
yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'redis://foo'], ['name' => 'bar', 'value' => 'redis://bar']]], ['enabled' => false, 'resources' => ['foo' => 'redis://foo', 'bar' => 'redis://bar']]];
317+
}
318+
268319
public function testItShowANiceMessageIfTwoMessengerBusesAreConfiguredButNoDefaultBus()
269320
{
270321
$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
524575
],
525576
],
526577
],
578+
'semaphore' => [
579+
'enabled' => !class_exists(FullStack::class),
580+
'resources' => [
581+
],
582+
],
527583
'messenger' => [
528584
'enabled' => !class_exists(FullStack::class) && interface_exists(MessageBusInterface::class),
529585
'routing' => [],
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" ?>
2+
<container xmlns="http://symfony.com/schema/dic/services"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:framework="http://symfony.com/schema/dic/symfony"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
6+
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
7+
8+
<framework:config>
9+
<framework:semaphore/>
10+
</framework:config>
11+
</container>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
framework:
2+
semaphore: redis://localhost
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
env(REDIS_DSN): redis://paas.com
3+
4+
framework:
5+
semaphore:
6+
foo: redis://paas.com
7+
qux: "%env(REDIS_DSN)%"

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"symfony/process": "^5.4|^6.0",
5454
"symfony/rate-limiter": "^5.4|^6.0",
5555
"symfony/security-bundle": "^5.4|^6.0",
56+
"symfony/semaphore": "^5.4|^6.0",
5657
"symfony/serializer": "^5.4|^6.0",
5758
"symfony/stopwatch": "^5.4|^6.0",
5859
"symfony/string": "^5.4|^6.0",

0 commit comments

Comments
 (0)