Skip to content

Commit c72c323

Browse files
committed
Integrated into the FrameworkBundle
1 parent 43881e7 commit c72c323

File tree

4 files changed

+261
-1
lines changed

4 files changed

+261
-1
lines changed

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

Lines changed: 65 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;
@@ -133,6 +134,7 @@ public function getConfigTreeBuilder()
133134
$this->addMailerSection($rootNode);
134135
$this->addSecretsSection($rootNode);
135136
$this->addNotifierSection($rootNode);
137+
$this->addRateLimiterSection($rootNode);
136138

137139
return $treeBuilder;
138140
}
@@ -1629,4 +1631,67 @@ private function addNotifierSection(ArrayNodeDefinition $rootNode)
16291631
->end()
16301632
;
16311633
}
1634+
1635+
private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
1636+
{
1637+
$rootNode
1638+
->children()
1639+
->arrayNode('rate_limiter')
1640+
->info('Rate limiter configuration')
1641+
->{!class_exists(FullStack::class) && class_exists(TokenBucketLimiter::class) ? 'canBeDisabled' : 'canBeEnabled'}()
1642+
->fixXmlConfig('limiter')
1643+
->beforeNormalization()
1644+
->ifTrue(function ($v) { return \is_array($v) && !isset($v['limiters']) && !isset($v['limiter']); })
1645+
->then(function (array $v) { return ['limiters' => $v]; })
1646+
->end()
1647+
->children()
1648+
->arrayNode('limiters')
1649+
->useAttributeAsKey('name')
1650+
->arrayPrototype()
1651+
->children()
1652+
->scalarNode('lock')->defaultNull()->end()
1653+
->scalarNode('storage')->isRequired()->end()
1654+
->arrayNode('bucket')
1655+
->children()
1656+
->integerNode('max_burst')->isRequired()->end()
1657+
->arrayNode('rate')
1658+
->children()
1659+
->scalarNode('interval')
1660+
->isRequired()
1661+
->cannotBeEmpty()
1662+
->validate()
1663+
->ifTrue(function ($v) {
1664+
return !preg_match('/P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?/', $v);
1665+
})
1666+
->thenInvalid('"interval" must be a valid DateInterval spec: P1DT2H3M4S')
1667+
->end()
1668+
->end()
1669+
->integerNode('amount')->defaultValue(1)->end()
1670+
->end()
1671+
->end()
1672+
->end()
1673+
->end()
1674+
->arrayNode('window')
1675+
->children()
1676+
->integerNode('limit')->isRequired()->end()
1677+
->scalarNode('interval')
1678+
->isRequired()
1679+
->cannotBeEmpty()
1680+
->validate()
1681+
->ifTrue(function ($v) {
1682+
return !preg_match('/P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?/', $v);
1683+
})
1684+
->thenInvalid('"interval" must be a valid DateInterval spec: P1DT2H3M4S')
1685+
->end()
1686+
->end()
1687+
->end()
1688+
->end()
1689+
->end()
1690+
->end()
1691+
->end()
1692+
->end()
1693+
->end()
1694+
->end()
1695+
;
1696+
}
16321697
}

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

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@
115115
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
116116
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
117117
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
118+
use Symfony\Component\RateLimiter\LimiterFactory;
119+
use Symfony\Component\RateLimiter\Rate;
120+
use Symfony\Component\RateLimiter\TokenBucketLimiter;
121+
use Symfony\Component\RateLimiter\Storage\CacheStorage;
122+
use Symfony\Component\RateLimiter\TokenBucket;
118123
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
119124
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
120125
use Symfony\Component\Security\Core\Security;
@@ -162,6 +167,7 @@ class FrameworkExtension extends Extension
162167
private $messengerConfigEnabled = false;
163168
private $mailerConfigEnabled = false;
164169
private $httpClientConfigEnabled = false;
170+
private $lockConfigEnabled = false;
165171

166172
/**
167173
* Responds to the app.config configuration parameter.
@@ -394,10 +400,18 @@ public function load(array $configs, ContainerBuilder $container)
394400
$this->registerPropertyInfoConfiguration($container, $loader);
395401
}
396402

397-
if ($this->isConfigEnabled($container, $config['lock'])) {
403+
if ($this->lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) {
398404
$this->registerLockConfiguration($config['lock'], $container, $loader);
399405
}
400406

407+
if ($this->isConfigEnabled($container, $config['rate_limiter'])) {
408+
if (!class_exists(TokenBucketLimiter::class)) {
409+
throw new LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
410+
}
411+
412+
$this->registerRateLimiterConfiguration($config['rate_limiter'], $container, $loader);
413+
}
414+
401415
if ($this->isConfigEnabled($container, $config['web_link'])) {
402416
if (!class_exists(HttpHeaderSerializer::class)) {
403417
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".');
@@ -2097,6 +2111,47 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
20972111
}
20982112
}
20992113

2114+
private function registerRateLimiterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2115+
{
2116+
if (!$this->lockConfigEnabled) {
2117+
throw new LogicException('Rate limiter support cannot be enabled without enabling the Lock component.');
2118+
}
2119+
2120+
$loader->load('rate_limiter.php');
2121+
2122+
$locks = [];
2123+
$storages = [];
2124+
foreach ($config['limiters'] as $name => $limiterConfig) {
2125+
$limiterFactory = $container->setDefinition($id = 'limiter.'.$name.'_factory', new ChildDefinition('limiter.factory'));
2126+
2127+
if (!isset($locks[$limiterConfig['lock']])) {
2128+
$locks[$limiterConfig['lock']] = new Reference($limiterConfig['lock']);
2129+
}
2130+
$limiterFactory->addArgument($locks[$limiterConfig['lock']]);
2131+
unset($limiterConfig['lock']);
2132+
2133+
if (!isset($storages[$limiterConfig['storage']])) {
2134+
$storageId = $limiterConfig['storage'];
2135+
if ($container->has($storageId)) {
2136+
// cache pools are configured by the FrameworkBundle, so they
2137+
// exists in the scoped ContainerBuilder provided to this method
2138+
if ($container->findDefinition($storageId)->hasTag('cache.pool')) {
2139+
$container->register('limiter.storage.'.$storageId, CacheStorage::class)->addArgument(new Reference($storageId));
2140+
$storageId = 'limiter.storage.'.$storageId;
2141+
}
2142+
}
2143+
2144+
$storages[$limiterConfig['storage']] = new Reference($storageId);
2145+
}
2146+
$limiterFactory->replaceArgument(1, $storages[$limiterConfig['storage']]);
2147+
unset($limiterConfig['storage']);
2148+
2149+
$limiterConfig['default_id'] = $name;
2150+
$limiterFactory->replaceArgument(0, $limiterConfig);
2151+
$container->registerAliasForArgument($id, LimiterFactory::class, $name.'_limiter_factory');
2152+
}
2153+
}
2154+
21002155
private function resolveTrustedHeaders(array $headers): int
21012156
{
21022157
$trustedHeaders = 0;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\LimiterFactory;
15+
use Symfony\Component\DependencyInjection\ServiceLocator;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('limiter.factory', LimiterFactory::class)
20+
->abstract()
21+
->args([
22+
abstract_arg('config'),
23+
abstract_arg('storage'),
24+
])
25+
;
26+
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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\Exception\InvalidOptionsException;
17+
use Symfony\Component\OptionsResolver\Options;
18+
use Symfony\Component\OptionsResolver\OptionsResolver;
19+
use Symfony\Component\RateLimiter\Storage\StorageInterface;
20+
use function foo\func;
21+
22+
/**
23+
* @author Wouter de Jong <wouter@wouterj.nl>
24+
*
25+
* @final
26+
*/
27+
class LimiterFactory
28+
{
29+
private $config;
30+
private $storage;
31+
private $lockFactory;
32+
33+
public function __construct(array $config, StorageInterface $storage, ?LockFactory $lockFactory)
34+
{
35+
$this->storage = $storage;
36+
$this->lockFactory = $lockFactory;
37+
38+
$options = new OptionsResolver();
39+
self::configureOptions($options);
40+
41+
$this->config = array_filter($options->resolve($config), function ($v) { return $v !== []; });
42+
43+
if (count($this->config) < 2) {
44+
throw new InvalidOptionsException('No limiter algorithm configured, it is required to set either "window" or "bucket" config options.');
45+
} elseif (count($this->config) > 2) {
46+
// indicates that more than one algorithm is configured
47+
unset($this->config['default_id']);
48+
49+
throw new InvalidOptionsException(sprintf('Cannot configure more than one limiter algorithm, but configuration is passed for: "%s".', implode('", "', array_keys($this->config))));
50+
}
51+
}
52+
53+
public function createLimiter(?string $id = null): LimiterInterface
54+
{
55+
$id = $id ?? $this->config['default_id'];
56+
$lock = $this->lockFactory ? $this->lockFactory->createLock($id) : new NoLock();
57+
58+
if (isset($this->config['bucket'])) {
59+
return new TokenBucketLimiter(new TokenBucket($id, $this->config['bucket']['max_burst'], $this->config['bucket']['rate']), $this->storage, $lock);
60+
}
61+
62+
// @todo
63+
//return new SlidingWindowLimiter(new Window($id, $this->config['window']));
64+
}
65+
66+
protected static function configureOptions(OptionsResolver $options): void
67+
{
68+
$options
69+
->define('default_id')->required()
70+
->define('bucket')
71+
->default(function (OptionsResolver $bucket) {
72+
$bucket
73+
->define('max_burst')->allowedTypes('int')
74+
->define('rate')
75+
->default(function (OptionsResolver $rate) {
76+
$rate
77+
->define('amount')->allowedTypes('int')
78+
->define('interval')->allowedValues(function ($interval) {
79+
return preg_match('/P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?/', $interval);
80+
})
81+
;
82+
})
83+
->normalize(function (Options $options, $value) {
84+
return new Rate(new \DateInterval($value['interval']), $value['amount']);
85+
})
86+
;
87+
})
88+
->allowedValues(function ($bucketValue) {
89+
if ([] === $bucketValue || (isset($bucketValue['max_burst']) && isset($bucketValue['rate']))) {
90+
return true;
91+
}
92+
93+
throw new InvalidOptionsException('The bucket[max_burst] and bucket[rate] options must be configured.');
94+
})
95+
96+
->define('window')
97+
->default(function (OptionsResolver $window) {
98+
$window
99+
->define('limit')->allowedTypes('int')
100+
->define('interval')->allowedValues(function ($interval) {
101+
return preg_match('/P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?/', $interval);
102+
})
103+
;
104+
})
105+
->allowedValues(function ($windowValue) {
106+
if ([] === $windowValue || (isset($windowValue['limit']) && isset($windowValue['sampling_period']))) {
107+
return true;
108+
}
109+
110+
throw new InvalidOptionsException('The window[limit] and window[interval] options must be configured.');
111+
})
112+
;
113+
}
114+
}

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