8000 [FrameworkBundle][RateLimiter] compound rate limiter config · symfony/symfony@ee2a127 · GitHub
[go: up one dir, main page]

Skip to content

Commit ee2a127

Browse files
kbondnicolas-grekas
authored andcommitted
[FrameworkBundle][RateLimiter] compound rate limiter config
1 parent 8e0d7dd commit ee2a127

File tree

4 files changed

+121
-3
lines changed

4 files changed

+121
-3
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ CHANGELOG
2424
* Deprecate setting the `framework.profiler.collect_serializer_data` config option to `false`
2525
* Set `framework.rate_limiter.limiters.*.lock_factory` to `auto` by default
2626
* Deprecate `RateLimiterFactory` autowiring aliases, use `RateLimiterFactoryInterface` instead
27+
* Allow configuring compound rate limiters
2728

2829
7.2
2930
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2518,7 +2518,12 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $
25182518
->enumNode('policy')
25192519
->info('The algorithm to be used by this limiter.')
25202520
->isRequired()
2521-
->values(['fixed_window', 'token_bucket', 'sliding_window', 'no_limit'])
2521+
->values(['fixed_window', 'token_bucket', 'sliding_window', 'compound', 'no_limit'])
2522+
->end()
2523+
->arrayNode('limiters')
2524+
->info('The limiter names to use when using the "compound" policy.')
2525+
->beforeNormalization()->castToArray()->end()
2526+
->scalarPrototype()->end()
25222527
->end()
25232528
->integerNode('limit')
25242529
->info('The maximum allowed hits in a fixed interval or burst.')
@@ -2537,8 +2542,8 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode, callable $
25372542
->end()
25382543
->end()
25392544
->validate()
2540-
->ifTrue(fn ($v) => 'no_limit' !== $v['policy'] && !isset($v['limit']))
2541-
->thenInvalid('A limit must be provided when using a policy different than "no_limit".')
2545+
->ifTrue(static fn ($v) => !\in_array($v['policy'], ['no_limit', 'compound']) && !isset($v['limit']))
2546+
->thenInvalid('A limit must be provided when using a policy different than "compound" or "no_limit".')
25422547
->end()
25432548
->end()
25442549
->end()

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
use Symfony\Component\Console\Debug\CliRequest;
6060
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
6161
use Symfony\Component\DependencyInjection\Alias;
62+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
6263
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
6364
use Symfony\Component\DependencyInjection\ChildDefinition;
6465
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
@@ -158,6 +159,7 @@
158159
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
159160
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
160161
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
162+
use Symfony\Component\RateLimiter\CompoundRateLimiterFactory;
161163
use Symfony\Component\RateLimiter\LimiterInterface;
162164
use Symfony\Component\RateLimiter\RateLimiterFactory;
163165
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
@@ -3232,7 +3234,18 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde
32323234
{
32333235
$loader->load('rate_limiter.php');
32343236

3237+
$limiters = [];
3238+
$compoundLimiters = [];
3239+
32353240
foreach ($config['limiters'] as $name => $limiterConfig) {
3241+
if ('compound' === $limiterConfig['policy']) {
3242+
$compoundLimiters[$name] = $limiterConfig;
3243+
3244+
continue;
3245+
}
3246+
3247+
$limiters[] = $name;
3248+
32363249
// default configuration (when used by other DI extensions)
32373250
$limiterConfig += ['lock_factory' => 'lock.factory', 'cache_pool' => 'cache.rate_limiter'];
32383251

@@ -3273,6 +3286,30 @@ private function registerRateLimiterConfiguration(array $config, ContainerBuilde
32733286
$factoryAlias->setDeprecated('symfony/dependency-injection', '7.3', 'The "%alias_id%" autowiring alias is deprecated and will be removed in 8.0, use "RateLimiterFactoryInterface" instead.');
32743287
}
32753288
}
3289+
3290+
if ($compoundLimiters && !class_exists(CompoundRateLimiterFactory::class)) {
3291+
throw new LogicException('Configuring compound rate limiters is only available in symfony/rate-limiter 7.3+.');
3292+
}
3293+
3294+
foreach ($compoundLimiters as $name => $limiterConfig) {
3295+
if (!$limiterConfig['limiters']) {
3296+
throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter.', $name));
3297+
}
3298+
3299+
if (\array_diff($limiterConfig['limiters'], $limiters)) {
3300+
throw new LogicException(\sprintf('Compound rate limiter "%s" requires at least one sub-limiter to be configured.', $name));
3301+
}
3302+
3303+
$container->register($limiterId = 'limiter.'.$name, CompoundRateLimiterFactory::class)
3304+
->addTag('rate_limiter', ['name' => $name])
3305+
->addArgument(new IteratorArgument(\array_map(
3306+
static fn (string $name) => new Reference('limiter.'.$name),
3307+
$limiterConfig['limiters']
3308+
)))
3309+
;
3310+
3311+
$container->registerAliasForArgument($limiterId, RateLimiterFactoryInterface::class, $name.'.limiter');
3312+
}
32763313
}
32773314

32783315
private function registerUidConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Symfony\Component\DependencyInjection\Exception\LogicException;
1818
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
1919
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
20+
use Symfony\Component\RateLimiter\CompoundRateLimiterFactory;
21+
use Symfony\Component\RateLimiter\RateLimiterFactoryInterface;
2022
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
2123

2224
class PhpFrameworkExtensionTest extends FrameworkExtensionTestCase
@@ -290,4 +292,77 @@ public function testRateLimiterIsTagged()
290292
$this->assertSame('first', $container->getDefinition('limiter.first')->getTag('rate_limiter')[0]['name']);
291293
$this->assertSame('second', $container->getDefinition('limiter.second')->getTag('rate_limiter')[0]['name']);
292294
}
295+
296+
public function testRateLimiterCompoundPolicy()
297+
{
298+
if (!class_exists(CompoundRateLimiterFactory::class)) {
299+
$this->markTestSkipped('CompoundRateLimiterFactory is not available.');
300+
}
301+
302+
$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
303+
$container->loadFromExtension('framework', [
304+
'annotations' => false,
305+
'http_method_override' => false,
306+
'handle_all_throwables' => true,
307+
'php_errors' => ['log' => true],
308+
'lock' => true,
309+
'rate_limiter' => [
310+
'first' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'],
311+
'second' => ['policy' => 'sliding_window', 'limit' => 10, 'interval' => '1 hour'],
312+
'compound' => ['policy' => 'compound', 'limiters' => ['first', 'second']],
313+
],
314+
]);
315+
});
316+
317+
$definition = $container->getDefinition('limiter.compound');
318+
$this->assertSame(CompoundRateLimiterFactory::class, $definition->getClass());
319+
$this->assertEquals(
320+
[
321+
'limiter.first',
322+
'limiter.second',
323+
],
324+
$definition->getArgument(0)->getValues()
325+
);
326+
$this->assertSame('limiter.compound', (string) $container->getAlias(RateLimiterFactoryInterface::class.' $compoundLimiter'));
327+
}
328+
329+
public function testRateLimiterCompoundPolicyNoLimiters()
330+
{
331+
if (!class_exists(CompoundRateLimiterFactory::class)) {
332+
$this->markTestSkipped('CompoundRateLimiterFactory is not available.');
333+
}
334+
335+
$this->expectException(\LogicException::class);
336+
$this->createContainerFromClosure(function ($container) {
337+
$container->loadFromExtension('framework', [
338+
'annotations' => false,
339+
'http_method_override' => false,
340+
'handle_all_throwables' => true,
341+
'php_errors' => ['log' => true],
342+
'rate_limiter' => [
343+
'compound' => ['policy' => 'compound'],
344+
],
345+
]);
346+
});
347+
}
348+
349+
public function testRateLimiterCompoundPolicyInvalidLimiters()
350+
{
351+
if (!class_exists(CompoundRateLimiterFactory::class)) {
352+
$this->markTestSkipped('CompoundRateLimiterFactory is not available.');
353+
}
354+
355+
$this->expectException(\LogicException::class);
356+
$this->createContainerFromClosure(function ($container) {
357+
$container->loadFromExtension('framework', [
358+
'annotations' => false,
359+
'http_method_override' => false,
360+
'handle_all_throwables' => true,
361+
'php_errors' => ['log' => true],
362+
'rate_limiter' => [
363+
'compound' => ['policy' => 'compound', 'limiters' => ['invalid1', 'invalid2']],
364+
],
365+
]);
366+
});
367+
}
293368
}

0 commit comments

Comments
 (0)
0