Skip to content

Commit fb9db12

Browse files
committed
Integrated into the FrameworkBundle
1 parent 7283137 commit fb9db12

File tree

9 files changed

+330
-2
lines changed

9 files changed

+330
-2
lines changed

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use Symfony\Component\Messenger\MessageBusInterface;
3131
use Symfony\Component\Notifier\Notifier;
3232
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
33+
use Symfony\Component\RateLimiter\TokenBucketLimiter;
3334
use Symfony\Component\Serializer\Serializer;
3435
use Symfony\Component\Translation\Translator;
3536
use Symfony\Component\Validator\Validation;
@@ -134,6 +135,7 @@ public function getConfigTreeBuilder()
134135
$this->addMailerSection($rootNode);
135136
$this->addSecretsSection($rootNode);
136137
$this->addNotifierSection($rootNode);
138+
$this->addRateLimiterSection($rootNode);
137139

138140
return $treeBuilder;
139141
}
@@ -1707,4 +1709,50 @@ private function addNotifierSection(ArrayNodeDefinition $rootNode)
17071709
->end()
17081710
;
17091711
}
1712+
1713+
private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
1714+
{
1715+
$rootNode
1716+
->children()
1717+
->arrayNode('rate_limiter')
1718+
->info('Rate limiter configuration')
1719+
->{!class_exists(FullStack::class) && class_exists(TokenBucketLimiter::class) ? 'canBeDisabled' : 'canBeEnabled'}()
1720+
->fixXmlConfig('limiter')
1721+
->beforeNormalization()
1722+
->ifTrue(function ($v) { return \is_array($v) && !isset($v['limiters']) && !isset($v['limiter']); })
1723+
->then(function (array $v) {
1724+
$newV = [
1725+
'enabled' => $v['enabled'],
1726+
];
1727+
unset($v['enabled']);
1728+
1729+
$newV['limiters'] = $v;
1730+
1731+
return $newV;
1732+
})
1733+
->end()
1734+
->children()
1735+
->arrayNode('limiters')
1736+
->useAttributeAsKey('name')
1737+
->arrayPrototype()
1738+
->children()
1739+
->scalarNode('lock')->defaultValue('lock.factory')->end()
1740+
->scalarNode('storage')->defaultValue('cache.app')->end()
1741+
->scalarNode('strategy')->isRequired()->end()
1742+
->integerNode('limit')->isRequired()->end()
1743+
->scalarNode('interval')->end()
1744+
->arrayNode('rate')
1745+
->children()
1746+
->scalarNode('interval')->isRequired()->end()
1747+
->integerNode('amount')->defaultValue(1)->end()
1748+
->end()
1749+
->end()
1750+
->end()
1751+
->end()
1752+
->end()
1753+
->end()
1754+
->end()
1755+
->end()
1756+
;
1757+
}
17101758
}

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@
123123
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
124124
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
125125
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
126+
use Symfony\Component\RateLimiter\Limiter;
127+
use Symfony\Component\RateLimiter\LimiterInterface;
128+
use Symfony\Component\RateLimiter\Storage\CacheStorage;
126129
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
127130
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
128131
use Symfony\Component\Security\Core\Security;
@@ -173,6 +176,7 @@ class FrameworkExtension extends Extension
173176
private $mailerConfigEnabled = false;
174177
private $httpClientConfigEnabled = false;
175178
private $notifierConfigEnabled = false;
179+
private $lockConfigEnabled = false;
176180

177181
/**
178182
* Responds to the app.config configuration parameter.
@@ -405,10 +409,18 @@ public function load(array $configs, ContainerBuilder $container)
405409
$this->registerPropertyInfoConfiguration($container, $loader);
406410
}
407411

408-
if ($this->isConfigEnabled($container, $config['lock'])) {
412+
if ($this->lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) {
409413
$this->registerLockConfiguration($config['lock'], $container, $loader);
410414
}
411415

416+
if ($this->isConfigEnabled($container, $config['rate_limiter'])) {
417+
if (!interface_exists(LimiterInterface::class)) {
418+
throw new LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
419+
}
420+
421+
$this->registerRateLimiterConfiguration($config['rate_limiter'], $container, $loader);
422+
}
423+
412424
if ($this->isConfigEnabled($container, $config['web_link'])) {
413425
if (!class_exists(HttpHeaderSerializer::class)) {
414426
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".');
@@ -2170,6 +2182,48 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
21702182
}
21712183
}
21722184

2185+
private function registerRateLimiterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2186+
{
2187+
if (!$this->lockConfigEnabled) {
2188+
throw new LogicException('Rate limiter support cannot be enabled without enabling the Lock component.');
2189+
}
2190+
2191+
$loader->load('rate_limiter.php');
2192+
2193+
$locks = [];
2194+
$storages = [];
2195+
foreach ($config['limiters'] as $name => $limiterConfig) {
2196+
$limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter'));
2197+
2198+
if (!isset($locks[$limiterConfig['lock']])) {
2199+
$locks[$limiterConfig['lock']] = new Reference($limiterConfig['lock']);
2200+
}
2201+
$limiter->addArgument($locks[$limiterConfig['lock']]);
2202+
unset($limiterConfig['lock']);
2203+
2204+
if (!isset($storages[$limiterConfig['storage']])) {
2205+
$storageId = $limiterConfig['storage'];
2206+
// cache pools are configured by the FrameworkBundle, so they
2207+
// exists in the scoped ContainerBuilder provided to this method
2208+
if ($container->has($storageId)) {
2209+
if ($container->findDefinition($storageId)->hasTag('cache.pool')) {
2210+
$container->register('limiter.storage.'.$storageId, CacheStorage::class)->addArgument(new Reference($storageId));
2211+
$storageId = 'limiter.storage.'.$storageId;
2212+
}
2213+
}
2214+
2215+
$storages[$limiterConfig['storage']] = new Reference($storageId);
2216+
}
2217+
$limiter->replaceArgument(1, $storages[$limiterConfig['storage']]);
2218+
unset($limiterConfig['storage']);
2219+
2220+
$limiterConfig['id'] = $name;
2221+
$limiter->replaceArgument(0, $limiterConfig);
2222+
2223+
$container->registerAliasForArgument($limiterId, Limiter::class, $name.'.limiter');
2224+
}
2225+
}
2226+
21732227
private function resolveTrustedHeaders(array $headers): int
21742228
{
21752229
$trustedHeaders = 0;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\RateLimiter\Limiter;
15+
16+
return static function (ContainerConfigurator $container) {
17+
$container->services()
18+
->set('limiter', Limiter::class)
19+
->abstract()
20+
->args([
21+
abstract_arg('config'),
22+
abstract_arg('storage'),
23+
])
24+
;
25+
};

src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<xsd:element name="http-client" type="http_client" minOccurs="0" maxOccurs="1" />
3535
<xsd:element name="mailer" type="mailer" minOccurs="0" maxOccurs="1" />
3636
<xsd:element name="http-cache" type="http_cache" minOccurs="0" maxOccurs="1" />
37+
<xsd:element name="rate-limiter" type="rate_limiter" minOccurs="0" maxOccurs="1" />
3738
</xsd:choice>
3839

3940
<xsd:attribute name="http-method-override" type="xsd:boolean" />
@@ -634,4 +635,30 @@
634635
<xsd:enumeration value="full" />
635636
</xsd:restriction>
636637
</xsd:simpleType>
638+
639+
<xsd:complexType name="rate_limiter">
640+
<xsd:sequence>
641+
<xsd:element name="limiter" type="rate_limiter_limiter" minOccurs="0" maxOccurs="unbounded" />
642+
</xsd:sequence>
643+
<xsd:attribute name="enabled" type="xsd:boolean" />
644+
<xsd:attribute name="max-host-connections" type="xsd:integer" />
645+
<xsd:attribute name="mock-response-factory" type="xsd:string" />
646+
</xsd:complexType>
647+
648+
<xsd:complexType name="rate_limiter_limiter">
649+
<xsd:sequence>
650+
<xsd:element name="rate" type="rate_limiter_rate" minOccurs="0" />
651+
</xsd:sequence>
652+
<xsd:attribute name="name" type="xsd:string" />
653+
<xsd:attribute name="lock" type="xsd:string" />
654+
<xsd:attribute name="storage" type="xsd:string" />
655+
<xsd:attribute name="strategy" type="xsd:string" />
656+
<xsd:attribute name="limit" type="xsd:int" />
657+
<xsd:attribute name="interval" type="xsd:string" />
658+
</xsd:complexType>
659+
660+
<xsd:complexType name="rate_limiter_rate">
661+
<xsd:attribute name="interval" type="xsd:string" />
662+
<xsd:attribute name="amount" type="xsd:int" />
663+
</xsd:complexType>
637664
</xsd:schema>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,10 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
531531
'debug' => '%kernel.debug%',
532532
'private_headers' => [],
533533
],
534+
'rate_limiter' => [
535+
'enabled' => false,
536+
'limiters' => [],
537+
],
534538
];
535539
}
536540
}

src/Symfony/Component/RateLimiter/CompoundLimiter.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ public function consume(int $tokens = 1): bool
3737

3838
return $allow;
3939
}
40+
41+
public function reset(): void
42+
{
43+
foreach ($this->limiters as $limiter) {
44+
$limiter->reset();
45+
}
46+
}
4047
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\RateLimiter;
13+
14+
use Symfony\Component\Lock\LockFactory;
15+
use Symfony\Component\Lock\NoLock;
16+
use Symfony\Component\OptionsResolver\Options;
17+
use Symfony\Component\OptionsResolver\OptionsResolver;
18+
use Symfony\Component\RateLimiter\Storage\StorageInterface;
19+
20+
/**
21+
* @author Wouter de Jong <wouter@wouterj.nl>
22+
*
23+
* @experimental in 5.2
24+
*/
25+
final class Limiter
26+
{
27+
private $config;
28+
private $storage;
29+
private $lockFactory;
30+
31+
public function __construct(array $config, StorageInterface $storage, ?LockFactory $lockFactory = null)
32+
{
33+
$this->storage = $storage;
34+
$this->lockFactory = $lockFactory;
35+
36+
$options = new OptionsResolver();
37+
self::configureOptions($options);
38+
39+
$this->config = $options->resolve($config);
40+
}
41+
42+
public function create(?string $key = null): LimiterInterface
43+
{
44+
$id = $this->config['id'].$key;
45+
$lock = $this->lockFactory ? $this->lockFactory->createLock($id) : new NoLock();
46+
47+
switch ($this->config['strategy']) {
48+
case 'token_bucket':
49+
return new TokenBucketLimiter($id, $this->config['limit'], $this->config['rate'], $this->storage, $lock);
50+
51+
case 'fixed_window':
52+
return new FixedWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock);
53+
54+
default:
55+
throw new \LogicException(sprintf('Limiter strategy "%s" does not exists, it must be either "token_bucket" or "fixed_window".', $this->config['strategy']));
56+
}
57+
}
58+
59+
protected static function configureOptions(OptionsResolver $options): void
60+
{
61+
$intervalNormalizer = static function (Options $options, string $interval): \DateInterval {
62+
try {
63+
return (new \DateTimeImmutable())->diff(new \DateTimeImmutable('+'.$interval));
64+
} catch (\Exception $e) {
65+
if (!preg_match('/Failed to parse time string \(\+([^)]+)\)/', $e->getMessage(), $m)) {
66+
throw $e;
67+
}
68+
69+
throw new \LogicException(sprintf('Cannot parse interval "%s", please use a valid unit as described on https://www.php.net/datetime.formats.relative.', $m[1]));
70+
}
71+
};
72+
73+
$options
74+
->define('id')->required()
75+
->define('strategy')
76+
->required()
77+
->allowedValues('token_bucket', 'fixed_window')
78+
79+
->define('limit')->allowedTypes('int')
80+
->define('interval')->allowedTypes('string')->normalize($intervalNormalizer)
81+
->define('rate')
82+
->default(function (OptionsResolver $rate) use ($intervalNormalizer) {
83+
$rate
84+
->define('amount')->allowedTypes('int')->default(1)
85+
->define('interval')->allowedTypes('string')->normalize($intervalNormalizer)
86+
;
87+
})
88+
->normalize(function (Options $options, $value) {
89+
if (!isset($value['interval'])) {
90+
return null;
91+
}
92+
93+
return new Rate($value['interval'], $value['amount']);
94+
})
95+
;
96+
}
97+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy