Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit ee2a127

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

File tree

4 files changed

+121
-3
lines changed
Filter options

4 files changed

+121
-3
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+8-3
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+37
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php
+75
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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.