Repository navigation
Expand file tree
/
Copy pathRateLimiterFactory.php
More file actions
127 lines (110 loc) · 5.57 KB
/
Copy pathRateLimiterFactory.php
File metadata and controls
127 lines (110 loc) · 5.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\RateLimiter;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\RateLimiter\Policy\FixedWindowLimiter;
use Symfony\Component\RateLimiter\Policy\NoLimiter;
use Symfony\Component\RateLimiter\Policy\Rate;
use Symfony\Component\RateLimiter\Policy\SlidingWindowLimiter;
use Symfony\Component\RateLimiter\Policy\TokenBucketLimiter;
use Symfony\Component\RateLimiter\Storage\StorageInterface;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
final class RateLimiterFactory implements RateLimiterFactoryInterface
{
private array $config;
public function __construct(
array $config,
private StorageInterface $storage,
private ?LockFactory $lockFactory = null,
) {
$options = new OptionsResolver();
self::configureOptions($options);
$this->config = $options->resolve($config);
}
public function create(?string $key = null): LimiterInterface
{
if ('no_limit' === $this->config['policy']) {
return new NoLimiter();
}
$id = $this->config['id'].'-'.$key;
$lock = $this->lockFactory?->createLock($id);
return match ($this->config['policy']) {
'token_bucket' => new TokenBucketLimiter($id, $this->config['limit'], $this->config['rate'], $this->storage, $lock),
'fixed_window' => new FixedWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock, $this->config['anchor_at']),
'sliding_window' => new SlidingWindowLimiter($id, $this->config['limit'], $this->config['interval'], $this->storage, $lock),
default => throw new \LogicException(\sprintf('Limiter policy "%s" does not exists, it must be either "token_bucket", "sliding_window", "fixed_window" or "no_limit".', $this->config['policy'])),
};
}
private static function configureOptions(OptionsResolver $options): void
{
$intervalNormalizer = static function (Options $options, \DateInterval|string $interval): \DateInterval {
if ($interval instanceof \DateInterval) {
return $interval;
}
// Create DateTimeImmutable from unix timestamp, so the default timezone is ignored and we don't need to
// deal with quirks happening when modifying dates using a timezone with DST.
$now = \DateTimeImmutable::createFromTimestamp(time());
try {
$nowPlusInterval = @$now->modify('+'.$interval);
} catch (\DateMalformedStringException $e) {
throw new \LogicException(\sprintf('Cannot parse interval "%s", please use a valid unit as described on https://php.net/datetime.formats#datetime.formats.relative', $interval), 0, $e);
}
if (!$nowPlusInterval) {
throw new \LogicException(\sprintf('Cannot parse interval "%s", please use a valid unit as described on https://php.net/datetime.formats#datetime.formats.relative', $interval));
}
return $now->diff($nowPlusInterval);
};
$options
->define('id')->required()
->define('policy')
->required()
->allowedValues('token_bucket', 'fixed_window', 'sliding_window', 'no_limit')
->define('limit')->allowedTypes('int')
->define('interval')->allowedTypes('string', \DateInterval::class)->normalize($intervalNormalizer)
->define('rate')
->options(static function (OptionsResolver $rate) use ($intervalNormalizer) {
$rate
->define('amount')->allowedTypes('int')->default(1)
->define('interval')->allowedTypes('string', \DateInterval::class)->normalize($intervalNormalizer)
;
})
->normalize(static function (Options $options, $value) {
if (!isset($value['interval'])) {
return null;
}
return new Rate($value['interval'], $value['amount']);
})
->define('anchor_at')
->default(null)
->allowedTypes('null', 'string', \DateTimeInterface::class)
->normalize(static function (Options $options, mixed $value): ?\DateTimeImmutable {
if (null === $value) {
return null;
}
if ('fixed_window' !== $options['policy']) {
throw new \LogicException('The "anchor_at" option is only supported with the "fixed_window" policy.');
}
if ($value instanceof \DateTimeInterface) {
return $value instanceof \DateTimeImmutable ? $value : \DateTimeImmutable::createFromInterface($value);
}
try {
// parse as UTC so timezone-less strings produce a stable timestamp across servers
return new \DateTimeImmutable($value, new \DateTimeZone('UTC'));
} catch (\Exception $e) {
throw new \InvalidArgumentException(\sprintf('Cannot parse "anchor_at" value "%s".', $value), 0, $e);
}
})
;
}
}