Skip to content

Commit a22ede9

Browse files
committed
Integrate into FrameworkBundle
1 parent 5461953 commit a22ede9

File tree

4 files changed

+183
-1
lines changed

4 files changed

+183
-1
lines changed

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

Lines changed: 46 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\Limiter;
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,48 @@ 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(Limiter::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+
->integerNode('initial_tokens')->defaultNull()->end()
1655+
->arrayNode('rate')
1656+
->children()
1657+
->scalarNode('interval')
1658+
->isRequired()
1659+
->cannotBeEmpty()
1660+
->validate()
1661+
->ifTrue(function ($v) {
1662+
return !preg_match('/P(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?/', $v);
1663+
})
1664+
->thenInvalid('"interval" must be a valid DateInterval spec: P1DT2H3M4S')
1665+
->end()
1666+
->end()
1667+
->integerNode('amount')->defaultValue(1)->end()
1668+
->end()
1669+
->end()
1670+
->end()
1671+
->end()
1672+
->end()
1673+
->end()
1674+
->end()
1675+
->end()
1676+
;
1677+
}
16321678
}

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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@
115115
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
116116
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
117117
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
118+
use Symfony\Component\RateLimiter\Limiter;
119+
use Symfony\Component\RateLimiter\Storage\CacheStorage;
120+
use Symfony\Component\RateLimiter\TokenBucket;
118121
use Symfony\Component\Routing\Loader\AnnotationDirectoryLoader;
119122
use Symfony\Component\Routing\Loader\AnnotationFileLoader;
120123
use Symfony\Component\Security\Core\Security;
@@ -162,6 +165,7 @@ class FrameworkExtension extends Extension
162165
private $messengerConfigEnabled = false;
163166
private $mailerConfigEnabled = false;
164167
private $httpClientConfigEnabled = false;
168+
private $lockConfigEnabled = false;
165169

166170
/**
167171
* Responds to the app.config configuration parameter.
@@ -394,10 +398,18 @@ public function load(array $configs, ContainerBuilder $container)
394398
$this->registerPropertyInfoConfiguration($container, $loader);
395399
}
396400

397-
if ($this->isConfigEnabled($container, $config['lock'])) {
401+
if ($this->lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) {
398402
$this->registerLockConfiguration($config['lock'], $container, $loader);
399403
}
400404

405+
if ($this->isConfigEnabled($container, $config['rate_limiter'])) {
406+
if (!class_exists(Limiter::class)) {
407+
throw new LogicException('Rate limiter support cannot be enabled as the RateLimiter component is not installed. Try running "composer require symfony/rate-limiter".');
408+
}
409+
410+
$this->registerRateLimiterConfiguration($config['rate_limiter'], $container, $loader);
411+
}
412+
401413
if ($this->isConfigEnabled($container, $config['web_link'])) {
402414
if (!class_exists(HttpHeaderSerializer::class)) {
403415
throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".');
@@ -2097,6 +2109,44 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
20972109
}
20982110
}
20992111

2112+
private function registerRateLimiterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
2113+
{
2114+
if (!$this->lockConfigEnabled) {
2115+
throw new LogicException('Rate limiter support cannot be enabled without enabling the Lock component.');
2116+
}
2117+
2118+
$loader->load('rate_limiter.php');
2119+
2120+
$limiterMap = [];
2121+
$storages = [];
2122+
$locks = [];
2123+
foreach ($config['limiters'] as $name => $limiterConfig) {
2124+
$limiterMap[$name] = $limiterConfig;
2125+
2126+
if (!isset($locks[$limiterConfig['lock']])) {
2127+
$locks[$limiterConfig['lock']] = new ServiceClosureArgument(new Reference($limiterConfig['lock']));
2128+
}
2129+
2130+
if (!isset($storages[$limiterConfig['storage']])) {
2131+
$storageId = $limiterConfig['storage'];
2132+
if ($container->has($storageId)) {
2133+
// cache pools are configured by the FrameworkBundle, so they
2134+
// exists in the scoped ContainerBuilder provided to this method
2135+
if ($container->findDefinition($storageId)->hasTag('cache.pool')) {
2136+
$container->register('limiter.storage.'.$storageId, CacheStorage::class)->addArgument(new Reference($storageId));
2137+
$storageId = 'limiter.storage.'.$storageId;
2138+
}
2139+
}
2140+
2141+
$storages[$limiterConfig['storage']] = new ServiceClosureArgument(new Reference($storageId));
2142+
}
2143+
}
2144+
2145+
$container->getDefinition('limiter.storage_locator')->addArgument($storages);
2146+
$container->getDefinition('limiter.lock_locator')->addArgument($locks);
2147+
$container->getDefinition('limiter.factory')->replaceArgument(0, $limiterMap);
2148+
}
2149+
21002150
private function resolveTrustedHeaders(array $headers): int
21012151
{
21022152
$trustedHeaders = 0;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\Bundle\FrameworkBundle\RateLimiter;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\Lock\LockFactory;
16+
use Symfony\Component\Lock\NoLock;
17+
use Symfony\Component\RateLimiter\Limiter;
18+
use Symfony\Component\RateLimiter\LimiterInterface;
19+
use Symfony\Component\RateLimiter\Rate;
20+
use Symfony\Component\RateLimiter\TokenBucket;
21+
22+
/**
23+
* @author Wouter de Jong <wouter@wouterj.nl>
24+
*
25+
* @final
26+
*/
27+
class LimiterFactory
28+
{
29+
private $limiterMap;
30+
private $storages;
31+
private $locks;
32+
33+
public function __construct(array $limiterMap, ContainerInterface $storages, ContainerInterface $locks)
34+
{
35+
$this->limiterMap = $limiterMap;
36+
$this->storages = $storages;
37+
$this->locks = $locks;
38+
}
39+
40+
public function createLimiter(string $name, ?string $id = null): LimiterInterface
41+
{
42+
if (!isset($this->limiterMap[$name])) {
43+
throw new \InvalidArgumentException(sprintf('Limiter "%s" is not set in the configuration, existing limiters are: "%s".', $name, implode('", "', array_keys($this->limiterMap))));
44+
}
45+
46+
$id = $id ?? uniqid();
47+
$config = $this->limiterMap[$name];
48+
$lock = null !== $config['lock'] ? $this->locks->get($config['lock'])->createLock($id) : new NoLock();
49+
$rate = new Rate(new \DateInterval($config['rate']['interval']), $config['rate']['amount']);
50+
51+
return new Limiter(new TokenBucket($id, $config['initial_tokens'] ?? $config['rate']['amount'], $rate), $this->storages->get($config['storage']), $lock);
52+
}
53+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\Bundle\FrameworkBundle\RateLimiter\LimiterFactory;
15+
use Symfony\Component\DependencyInjection\ServiceLocator;
16+
17+
return static function (ContainerConfigurator $container) {
18+
$container->services()
19+
->set('limiter.storage_locator', ServiceLocator::class)
20+
->args([])
21+
22+
->set('limiter.lock_locator', ServiceLocator::class)
23+
->args([])
24+
25+
->set('limiter.factory', LimiterFactory::class)
26+
->args([
27+
abstract_arg('limiter map'),
28+
service('limiter.storage_locator'),
29+
service('limiter.lock_locator'),
30+
])
31+
->alias(LimiterFactory::class, 'limiter.factory')
32+
;
33+
};

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