From 91e53ec4cc93efe63de5de0ad716fe6a8fa038b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:01:44 +0000 Subject: [PATCH 01/39] Initial plan From fd3b8b2e50f51a1eff0178eac57140ec9a636185 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:12:52 +0000 Subject: [PATCH 02/39] Add rate-limit component with core algorithms and features Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- composer.json | 3 + src/rate-limit/.gitattributes | 4 + src/rate-limit/LICENSE | 22 ++ src/rate-limit/README.md | 192 ++++++++++++++++++ src/rate-limit/composer.json | 53 +++++ src/rate-limit/publish/rate_limit.php | 59 ++++++ .../src/Algorithm/FixedWindowRateLimiter.php | 68 +++++++ .../src/Algorithm/LeakyBucketRateLimiter.php | 70 +++++++ .../Algorithm/SlidingWindowRateLimiter.php | 67 ++++++ .../src/Algorithm/TokenBucketRateLimiter.php | 70 +++++++ src/rate-limit/src/Annotation/RateLimit.php | 37 ++++ src/rate-limit/src/Aspect/RateLimitAspect.php | 127 ++++++++++++ src/rate-limit/src/ConfigProvider.php | 37 ++++ .../src/Contract/RateLimiterInterface.php | 67 ++++++ .../src/Exception/RateLimitException.php | 18 ++ .../src/Middleware/RateLimitMiddleware.php | 142 +++++++++++++ src/rate-limit/src/RateLimiterFactory.php | 60 ++++++ src/rate-limit/src/Storage/LuaScripts.php | 157 ++++++++++++++ 18 files changed, 1253 insertions(+) create mode 100644 src/rate-limit/.gitattributes create mode 100644 src/rate-limit/LICENSE create mode 100644 src/rate-limit/README.md create mode 100644 src/rate-limit/composer.json create mode 100644 src/rate-limit/publish/rate_limit.php create mode 100644 src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php create mode 100644 src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php create mode 100644 src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php create mode 100644 src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php create mode 100644 src/rate-limit/src/Annotation/RateLimit.php create mode 100644 src/rate-limit/src/Aspect/RateLimitAspect.php create mode 100644 src/rate-limit/src/ConfigProvider.php create mode 100644 src/rate-limit/src/Contract/RateLimiterInterface.php create mode 100644 src/rate-limit/src/Exception/RateLimitException.php create mode 100644 src/rate-limit/src/Middleware/RateLimitMiddleware.php create mode 100644 src/rate-limit/src/RateLimiterFactory.php create mode 100644 src/rate-limit/src/Storage/LuaScripts.php diff --git a/composer.json b/composer.json index 222ea3af0..e52319257 100644 --- a/composer.json +++ b/composer.json @@ -138,6 +138,7 @@ "friendsofhyperf/ipc-broadcaster": "*", "friendsofhyperf/lock": "*", "friendsofhyperf/macros": "*", + "friendsofhyperf/rate-limit": "*", "friendsofhyperf/mail": "*", "friendsofhyperf/model-factory": "*", "friendsofhyperf/model-hashids": "*", @@ -192,6 +193,7 @@ "FriendsOfHyperf\\Lock\\": "src/lock/src/", "FriendsOfHyperf\\Macros\\": "src/macros/src/", "FriendsOfHyperf\\Mail\\": "src/mail/src/", + "FriendsOfHyperf\\RateLimit\\": "src/rate-limit/src/", "FriendsOfHyperf\\ModelFactory\\": "src/model-factory/src/", "FriendsOfHyperf\\ModelHashids\\": "src/model-hashids/src/", "FriendsOfHyperf\\ModelMorphAddon\\": "src/model-morph-addon/src/", @@ -272,6 +274,7 @@ "FriendsOfHyperf\\Lock\\ConfigProvider", "FriendsOfHyperf\\Macros\\ConfigProvider", "FriendsOfHyperf\\Mail\\ConfigProvider", + "FriendsOfHyperf\\RateLimit\\ConfigProvider", "FriendsOfHyperf\\ModelFactory\\ConfigProvider", "FriendsOfHyperf\\ModelHashids\\ConfigProvider", "FriendsOfHyperf\\ModelMorphAddon\\ConfigProvider", diff --git a/src/rate-limit/.gitattributes b/src/rate-limit/.gitattributes new file mode 100644 index 000000000..c90148b0e --- /dev/null +++ b/src/rate-limit/.gitattributes @@ -0,0 +1,4 @@ +/.github export-ignore +/.vscode export-ignore +/tests export-ignore +.gitattributes export-ignore \ No newline at end of file diff --git a/src/rate-limit/LICENSE b/src/rate-limit/LICENSE new file mode 100644 index 000000000..ef4cc321f --- /dev/null +++ b/src/rate-limit/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) Taylor Otwell +Copyright (c) D.J.Hwang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/rate-limit/README.md b/src/rate-limit/README.md new file mode 100644 index 000000000..6ff6dc25d --- /dev/null +++ b/src/rate-limit/README.md @@ -0,0 +1,192 @@ +# Rate Limit + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) +[![Total Downloads](https://img.shields.io/packagist/dt/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) + +Rate limiting component for Hyperf with support for multiple algorithms. + +## Features + +- **Multiple Algorithms**: Fixed Window, Sliding Window, Token Bucket, Leaky Bucket +- **Annotation Support**: Easy declarative rate limiting with `#[RateLimit]` annotation +- **AOP Integration**: Automatic rate limiting via aspect-oriented programming +- **Middleware**: HTTP middleware for request rate limiting +- **Redis + Lua**: Atomic operations using Redis with Lua scripts +- **Dynamic Configuration**: Support for config center integration +- **Flexible Key Resolution**: Support for placeholders like `{ip}`, `{user_id}`, etc. + +## Installation + +```bash +composer require friendsofhyperf/rate-limit +``` + +## Publish Configuration + +```bash +php bin/hyperf.php vendor:publish friendsofhyperf/rate-limit +``` + +This will create a `config/autoload/rate_limit.php` configuration file. + +## Usage + +### Using Annotations + +```php +use FriendsOfHyperf\RateLimit\Annotation\RateLimit; + +class UserController +{ + #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: "sliding_window")] + public function index() + { + return ['message' => 'Hello World']; + } + + #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: "fixed_window")] + public function login() + { + // Login logic + } +} +``` + +### Using Middleware + +Create a custom middleware that extends `RateLimitMiddleware`: + +```php +namespace App\Middleware; + +use FriendsOfHyperf\RateLimit\Middleware\RateLimitMiddleware; +use Psr\Http\Message\ServerRequestInterface; + +class ApiRateLimitMiddleware extends RateLimitMiddleware +{ + protected int $maxAttempts = 60; + protected int $decay = 60; + protected string $algorithm = 'sliding_window'; + + protected function resolveKey(ServerRequestInterface $request): string + { + return 'api:' . $this->getClientIp(); + } +} +``` + +Then register it in your middleware configuration. + +### Using Directly in Code + +```php +use FriendsOfHyperf\RateLimit\RateLimiterFactory; + +class YourService +{ + public function __construct(private RateLimiterFactory $factory) + { + } + + public function someMethod() + { + $limiter = $this->factory->make('sliding_window'); + + $key = 'operation:user:123'; + $maxAttempts = 10; + $decay = 60; + + if ($limiter->tooManyAttempts($key, $maxAttempts, $decay)) { + throw new \Exception('Rate limit exceeded'); + } + + // Your logic here + } +} +``` + +## Algorithms + +### Fixed Window + +Simple counter that resets at fixed intervals. Fast but can allow bursts at window boundaries. + +```php +#[RateLimit(algorithm: "fixed_window", maxAttempts: 100, decay: 60)] +``` + +### Sliding Window + +More accurate than fixed window, uses sorted sets to track requests with timestamps. + +```php +#[RateLimit(algorithm: "sliding_window", maxAttempts: 100, decay: 60)] +``` + +### Token Bucket + +Allows bursts up to bucket capacity, tokens are added at a constant rate. + +```php +#[RateLimit(algorithm: "token_bucket", maxAttempts: 100, decay: 60)] +``` + +### Leaky Bucket + +Smooths out bursts, processes requests at a constant rate regardless of arrival pattern. + +```php +#[RateLimit(algorithm: "leaky_bucket", maxAttempts: 100, decay: 60)] +``` + +## Configuration + +The configuration file supports: + +```php +return [ + 'default' => 'fixed_window', + 'connection' => 'default', + 'prefix' => 'rate_limit', + + 'defaults' => [ + 'max_attempts' => 60, + 'decay' => 60, + ], + + 'limiters' => [ + 'api' => [ + 'max_attempts' => 60, + 'decay' => 60, + 'algorithm' => 'sliding_window', + ], + 'login' => [ + 'max_attempts' => 5, + 'decay' => 60, + 'algorithm' => 'fixed_window', + ], + ], +]; +``` + +## Key Placeholders + +The annotation supports dynamic placeholders in the key: + +- `{ip}` - Client IP address +- `{user_id}` - User ID from request attributes +- Any method argument name + +Example: + +```php +#[RateLimit(key: "api:{ip}:user:{user_id}", maxAttempts: 60, decay: 60)] +public function profile($userId) +{ + // ... +} +``` + +## License + +MIT diff --git a/src/rate-limit/composer.json b/src/rate-limit/composer.json new file mode 100644 index 000000000..4a6ab9a90 --- /dev/null +++ b/src/rate-limit/composer.json @@ -0,0 +1,53 @@ +{ + "name": "friendsofhyperf/rate-limit", + "description": "Rate limiting component for Hyperf with support for multiple algorithms (Fixed Window, Sliding Window, Token Bucket, Leaky Bucket).", + "license": "MIT", + "type": "library", + "keywords": [ + "hyperf", + "rate-limit", + "throttle", + "v3.1" + ], + "authors": [ + { + "name": "huangdijia", + "email": "huangdijia@gmail.com" + } + ], + "support": { + "issues": "https://github.com/friendsofhyperf/components/issues", + "source": "https://github.com/friendsofhyperf/components", + "docs": "https://hyperf.fans", + "pull-request": "https://github.com/friendsofhyperf/components/pulls" + }, + "require": { + "php": ">=8.1", + "hyperf/config": "~3.1.0", + "hyperf/context": "~3.1.0", + "hyperf/di": "~3.1.0", + "hyperf/redis": "~3.1.0", + "hyperf/support": "~3.1.0" + }, + "suggest": { + "hyperf/config-center": "Required for dynamic configuration support.", + "hyperf/http-server": "Required for middleware support." + }, + "autoload": { + "psr-4": { + "FriendsOfHyperf\\RateLimit\\": "src" + } + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true + }, + "extra": { + "branch-alias": { + "dev-main": "3.1-dev" + }, + "hyperf": { + "config": "FriendsOfHyperf\\RateLimit\\ConfigProvider" + } + } +} diff --git a/src/rate-limit/publish/rate_limit.php b/src/rate-limit/publish/rate_limit.php new file mode 100644 index 000000000..8d45a5ede --- /dev/null +++ b/src/rate-limit/publish/rate_limit.php @@ -0,0 +1,59 @@ + env('RATE_LIMIT_ALGORITHM', 'fixed_window'), + + /* + * Redis connection name + * Uses the default Redis connection if not specified + */ + 'connection' => env('RATE_LIMIT_REDIS_CONNECTION', 'default'), + + /* + * Prefix for rate limit keys in Redis + */ + 'prefix' => env('RATE_LIMIT_PREFIX', 'rate_limit'), + + /* + * Default rate limit settings + */ + 'defaults' => [ + 'max_attempts' => 60, + 'decay' => 60, // seconds + ], + + /* + * Named rate limiters + * You can define custom rate limiters here + */ + 'limiters' => [ + 'api' => [ + 'max_attempts' => 60, + 'decay' => 60, + 'algorithm' => 'sliding_window', + ], + 'login' => [ + 'max_attempts' => 5, + 'decay' => 60, + 'algorithm' => 'fixed_window', + ], + 'global' => [ + 'max_attempts' => 1000, + 'decay' => 60, + 'algorithm' => 'token_bucket', + ], + ], +]; diff --git a/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php b/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php new file mode 100644 index 000000000..e48dc64d8 --- /dev/null +++ b/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php @@ -0,0 +1,68 @@ +redis->eval( + LuaScripts::fixedWindow(), + [$this->getKey($key)], + [$maxAttempts, $decay, time()], + 1 + ); + + return (bool) $result[0]; + } + + public function tooManyAttempts(string $key, int $maxAttempts, int $decay): bool + { + return ! $this->attempt($key, $maxAttempts, $decay); + } + + public function attempts(string $key): int + { + $value = $this->redis->get($this->getKey($key)); + return $value ? (int) $value : 0; + } + + public function remaining(string $key, int $maxAttempts): int + { + $attempts = $this->attempts($key); + return max(0, $maxAttempts - $attempts); + } + + public function clear(string $key): void + { + $this->redis->del($this->getKey($key)); + } + + public function availableIn(string $key): int + { + $ttl = $this->redis->ttl($this->getKey($key)); + return $ttl > 0 ? $ttl : 0; + } + + protected function getKey(string $key): string + { + return $this->prefix . ':fixed:' . $key; + } +} diff --git a/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php new file mode 100644 index 000000000..b6056a466 --- /dev/null +++ b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php @@ -0,0 +1,70 @@ +redis->eval( + LuaScripts::leakyBucket(), + [$this->getKey($key)], + [$maxAttempts, $leakRate, microtime(true)], + 1 + ); + + return (bool) $result[0]; + } + + public function tooManyAttempts(string $key, int $maxAttempts, int $decay): bool + { + return ! $this->attempt($key, $maxAttempts, $decay); + } + + public function attempts(string $key): int + { + $water = $this->redis->hGet($this->getKey($key), 'water'); + return $water ? (int) $water : 0; + } + + public function remaining(string $key, int $maxAttempts): int + { + $water = $this->attempts($key); + return max(0, $maxAttempts - $water); + } + + public function clear(string $key): void + { + $this->redis->del($this->getKey($key)); + } + + public function availableIn(string $key): int + { + $ttl = $this->redis->ttl($this->getKey($key)); + return $ttl > 0 ? $ttl : 0; + } + + protected function getKey(string $key): string + { + return $this->prefix . ':leaky:' . $key; + } +} diff --git a/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php b/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php new file mode 100644 index 000000000..3900768bb --- /dev/null +++ b/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php @@ -0,0 +1,67 @@ +redis->eval( + LuaScripts::slidingWindow(), + [$this->getKey($key)], + [$maxAttempts, $decay, microtime(true)], + 1 + ); + + return (bool) $result[0]; + } + + public function tooManyAttempts(string $key, int $maxAttempts, int $decay): bool + { + return ! $this->attempt($key, $maxAttempts, $decay); + } + + public function attempts(string $key): int + { + return (int) $this->redis->zCard($this->getKey($key)); + } + + public function remaining(string $key, int $maxAttempts): int + { + $attempts = $this->attempts($key); + return max(0, $maxAttempts - $attempts); + } + + public function clear(string $key): void + { + $this->redis->del($this->getKey($key)); + } + + public function availableIn(string $key): int + { + $ttl = $this->redis->ttl($this->getKey($key)); + return $ttl > 0 ? $ttl : 0; + } + + protected function getKey(string $key): string + { + return $this->prefix . ':sliding:' . $key; + } +} diff --git a/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php new file mode 100644 index 000000000..a5d1affe1 --- /dev/null +++ b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php @@ -0,0 +1,70 @@ +redis->eval( + LuaScripts::tokenBucket(), + [$this->getKey($key)], + [$maxAttempts, $refillRate, 1, microtime(true)], + 1 + ); + + return (bool) $result[0]; + } + + public function tooManyAttempts(string $key, int $maxAttempts, int $decay): bool + { + return ! $this->attempt($key, $maxAttempts, $decay); + } + + public function attempts(string $key): int + { + $bucket = $this->redis->hGet($this->getKey($key), 'tokens'); + return $bucket ? (int) $bucket : 0; + } + + public function remaining(string $key, int $maxAttempts): int + { + $tokens = $this->attempts($key); + return max(0, (int) $tokens); + } + + public function clear(string $key): void + { + $this->redis->del($this->getKey($key)); + } + + public function availableIn(string $key): int + { + $ttl = $this->redis->ttl($this->getKey($key)); + return $ttl > 0 ? $ttl : 0; + } + + protected function getKey(string $key): string + { + return $this->prefix . ':token:' . $key; + } +} diff --git a/src/rate-limit/src/Annotation/RateLimit.php b/src/rate-limit/src/Annotation/RateLimit.php new file mode 100644 index 000000000..14ff8e58c --- /dev/null +++ b/src/rate-limit/src/Annotation/RateLimit.php @@ -0,0 +1,37 @@ +getAnnotationMetadata(); + + /** @var RateLimit|null $annotation */ + $annotation = $metadata->method[RateLimit::class] + ?? $metadata->class[RateLimit::class] + ?? null; + + if (! $annotation) { + return $proceedingJoinPoint->process(); + } + + $key = $this->resolveKey($annotation->key, $proceedingJoinPoint); + $limiter = $this->factory->make($annotation->algorithm); + + if ($limiter->tooManyAttempts($key, $annotation->maxAttempts, $annotation->decay)) { + $availableIn = $limiter->availableIn($key); + throw new RateLimitException( + sprintf( + '%s Please try again in %d seconds.', + $annotation->response, + $availableIn + ) + ); + } + + return $proceedingJoinPoint->process(); + } + + protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPoint): string + { + if (empty($key)) { + // Use method signature as default key + $className = $proceedingJoinPoint->className; + $methodName = $proceedingJoinPoint->methodName; + $key = "{$className}:{$methodName}"; + } + + // Support placeholders like {user_id}, {ip}, etc. + if (str_contains($key, '{')) { + $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { + $placeholder = $matches[1]; + + // Try to get from request + if ($placeholder === 'ip') { + return $this->getClientIp(); + } + + if ($placeholder === 'user_id') { + return $this->getUserId(); + } + + // Try to get from method arguments + $arguments = $proceedingJoinPoint->arguments; + foreach ($arguments['keys'] ?? [] as $argKey => $argValue) { + if ($argKey === $placeholder) { + return (string) $argValue; + } + } + + return $matches[0]; + }, $key); + } + + return $key; + } + + protected function getClientIp(): string + { + $headers = [ + 'x-forwarded-for', + 'x-real-ip', + 'remote-addr', + ]; + + foreach ($headers as $header) { + if ($ip = $this->request->getHeaderLine($header)) { + // Get first IP if comma-separated list + if (str_contains($ip, ',')) { + $ip = trim(explode(',', $ip)[0]); + } + return $ip; + } + } + + return $this->request->server('remote_addr', 'unknown'); + } + + protected function getUserId(): string + { + // This is a placeholder - should be customized based on your auth system + return (string) ($this->request->getAttribute('user_id') ?? 'guest'); + } +} diff --git a/src/rate-limit/src/ConfigProvider.php b/src/rate-limit/src/ConfigProvider.php new file mode 100644 index 000000000..f8867e3a3 --- /dev/null +++ b/src/rate-limit/src/ConfigProvider.php @@ -0,0 +1,37 @@ + [ + // Add dependencies here + ], + 'aspects' => [ + RateLimitAspect::class, + ], + 'publish' => [ + [ + 'id' => 'config', + 'description' => 'The configuration file of rate-limit.', + 'source' => __DIR__ . '/../publish/rate_limit.php', + 'destination' => BASE_PATH . '/config/autoload/rate_limit.php', + ], + ], + ]; + } +} diff --git a/src/rate-limit/src/Contract/RateLimiterInterface.php b/src/rate-limit/src/Contract/RateLimiterInterface.php new file mode 100644 index 000000000..1aebfe3f6 --- /dev/null +++ b/src/rate-limit/src/Contract/RateLimiterInterface.php @@ -0,0 +1,67 @@ +resolveKey($request); + $limiter = $this->factory->make($this->algorithm); + + if ($limiter->tooManyAttempts($key, $this->maxAttempts, $this->decay)) { + return $this->buildRateLimitExceededResponse($key, $limiter->availableIn($key)); + } + + $response = $handler->handle($request); + + return $this->addHeaders( + $response, + $this->maxAttempts, + $limiter->remaining($key, $this->maxAttempts), + $limiter->availableIn($key) + ); + } + + /** + * Resolve the rate limit key. + */ + protected function resolveKey(ServerRequestInterface $request): string + { + // Default key based on IP address + return 'rate_limit:' . $this->getClientIp(); + } + + /** + * Get the client IP address. + */ + protected function getClientIp(): string + { + $headers = [ + 'x-forwarded-for', + 'x-real-ip', + 'remote-addr', + ]; + + foreach ($headers as $header) { + if ($ip = $this->request->getHeaderLine($header)) { + // Get first IP if comma-separated list + if (str_contains($ip, ',')) { + $ip = trim(explode(',', $ip)[0]); + } + return $ip; + } + } + + return $this->request->server('remote_addr', 'unknown'); + } + + /** + * Build rate limit exceeded response. + */ + protected function buildRateLimitExceededResponse(string $key, int $retryAfter): ResponseInterface + { + return $this->response + ->withStatus($this->responseCode) + ->withAddedHeader('Retry-After', (string) $retryAfter) + ->withAddedHeader('X-RateLimit-Limit', (string) $this->maxAttempts) + ->withAddedHeader('X-RateLimit-Remaining', '0') + ->withAddedHeader('X-RateLimit-Reset', (string) (time() + $retryAfter)) + ->json([ + 'message' => $this->responseMessage, + 'retry_after' => $retryAfter, + ]); + } + + /** + * Add rate limit headers to response. + */ + protected function addHeaders( + ResponseInterface $response, + int $maxAttempts, + int $remaining, + int $retryAfter + ): ResponseInterface { + return $response + ->withAddedHeader('X-RateLimit-Limit', (string) $maxAttempts) + ->withAddedHeader('X-RateLimit-Remaining', (string) max(0, $remaining)) + ->withAddedHeader('X-RateLimit-Reset', (string) (time() + $retryAfter)); + } +} diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php new file mode 100644 index 000000000..afa9bcee3 --- /dev/null +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -0,0 +1,60 @@ +limiters[$key])) { + return $this->limiters[$key]; + } + + $redis = $this->getRedis($connection); + $prefix = $this->getPrefix(); + + return $this->limiters[$key] = match ($algorithm) { + 'fixed_window' => new FixedWindowRateLimiter($redis, $prefix), + 'sliding_window' => new SlidingWindowRateLimiter($redis, $prefix), + 'token_bucket' => new TokenBucketRateLimiter($redis, $prefix), + 'leaky_bucket' => new LeakyBucketRateLimiter($redis, $prefix), + default => throw new RateLimitException("Unsupported rate limiter algorithm: {$algorithm}"), + }; + } + + protected function getRedis(?string $connection = null): Redis + { + return $this->container->get(Redis::class); + } + + protected function getPrefix(): string + { + return 'rate_limit'; + } +} diff --git a/src/rate-limit/src/Storage/LuaScripts.php b/src/rate-limit/src/Storage/LuaScripts.php new file mode 100644 index 000000000..0bf756e3b --- /dev/null +++ b/src/rate-limit/src/Storage/LuaScripts.php @@ -0,0 +1,157 @@ += max_attempts then + return {0, tonumber(current), redis.call('ttl', key)} +end + +local count = redis.call('incr', key) +if count == 1 then + redis.call('expire', key, decay) +end + +return {1, count, redis.call('ttl', key)} +LUA; + } + + /** + * Sliding window Lua script. + * Uses sorted set to track requests with timestamps. + */ + public static function slidingWindow(): string + { + return <<<'LUA' +local key = KEYS[1] +local max_attempts = tonumber(ARGV[1]) +local decay = tonumber(ARGV[2]) +local current_time = tonumber(ARGV[3]) + +local window_start = current_time - decay + +-- Remove old entries outside the time window +redis.call('zremrangebyscore', key, '-inf', window_start) + +-- Count current entries in the window +local current = redis.call('zcard', key) + +if current >= max_attempts then + local oldest = redis.call('zrange', key, 0, 0, 'WITHSCORES') + local ttl = 0 + if #oldest > 0 then + ttl = math.ceil(tonumber(oldest[2]) + decay - current_time) + end + return {0, current, ttl} +end + +-- Add current request +redis.call('zadd', key, current_time, current_time) +redis.call('expire', key, decay + 1) + +return {1, current + 1, decay} +LUA; + } + + /** + * Token bucket Lua script. + * Implements token bucket algorithm. + */ + public static function tokenBucket(): string + { + return <<<'LUA' +local key = KEYS[1] +local capacity = tonumber(ARGV[1]) +local refill_rate = tonumber(ARGV[2]) +local requested = tonumber(ARGV[3]) +local current_time = tonumber(ARGV[4]) + +local bucket = redis.call('hmget', key, 'tokens', 'last_refill') +local tokens = tonumber(bucket[1]) +local last_refill = tonumber(bucket[2]) + +if tokens == nil then + tokens = capacity + last_refill = current_time +end + +-- Calculate tokens to add based on time elapsed +local time_elapsed = current_time - last_refill +local tokens_to_add = time_elapsed * refill_rate +tokens = math.min(capacity, tokens + tokens_to_add) + +if tokens >= requested then + tokens = tokens - requested + redis.call('hmset', key, 'tokens', tokens, 'last_refill', current_time) + redis.call('expire', key, 3600) + return {1, math.floor(tokens), 0} +else + local tokens_needed = requested - tokens + local wait_time = math.ceil(tokens_needed / refill_rate) + return {0, math.floor(tokens), wait_time} +end +LUA; + } + + /** + * Leaky bucket Lua script. + * Implements leaky bucket algorithm. + */ + public static function leakyBucket(): string + { + return <<<'LUA' +local key = KEYS[1] +local capacity = tonumber(ARGV[1]) +local leak_rate = tonumber(ARGV[2]) +local current_time = tonumber(ARGV[3]) + +local bucket = redis.call('hmget', key, 'water', 'last_leak') +local water = tonumber(bucket[1]) +local last_leak = tonumber(bucket[2]) + +if water == nil then + water = 0 + last_leak = current_time +end + +-- Calculate water leaked based on time elapsed +local time_elapsed = current_time - last_leak +local water_leaked = time_elapsed * leak_rate +water = math.max(0, water - water_leaked) + +if water < capacity then + water = water + 1 + redis.call('hmset', key, 'water', water, 'last_leak', current_time) + redis.call('expire', key, 3600) + local remaining = capacity - water + return {1, math.floor(remaining), 0} +else + local wait_time = math.ceil((water - capacity + 1) / leak_rate) + return {0, 0, wait_time} +end +LUA; + } +} From 49a675bedc2a046ce719306acac6f26f08f93339 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:15:22 +0000 Subject: [PATCH 03/39] Add tests and Chinese documentation for rate-limit component Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- src/rate-limit/README_CN.md | 192 +++++++++++++++++++++ tests/Pest.php | 1 + tests/RateLimit/AnnotationTest.php | 46 +++++ tests/RateLimit/LuaScriptsTest.php | 47 +++++ tests/RateLimit/RateLimiterFactoryTest.php | 101 +++++++++++ 5 files changed, 387 insertions(+) create mode 100644 src/rate-limit/README_CN.md create mode 100644 tests/RateLimit/AnnotationTest.php create mode 100644 tests/RateLimit/LuaScriptsTest.php create mode 100644 tests/RateLimit/RateLimiterFactoryTest.php diff --git a/src/rate-limit/README_CN.md b/src/rate-limit/README_CN.md new file mode 100644 index 000000000..643dc5cee --- /dev/null +++ b/src/rate-limit/README_CN.md @@ -0,0 +1,192 @@ +# 限流组件 + +[![Latest Version on Packagist](https://img.shields.io/packagist/v/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) +[![Total Downloads](https://img.shields.io/packagist/dt/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) + +为 Hyperf 提供的限流组件,支持多种限流算法。 + +## 特性 + +- **多种算法**:固定窗口、滑动窗口、令牌桶、漏桶 +- **注解支持**:使用 `#[RateLimit]` 注解轻松实现声明式限流 +- **AOP 集成**:通过面向切面编程自动限流 +- **中间件**:提供 HTTP 请求限流中间件 +- **Redis + Lua**:使用 Redis 和 Lua 脚本实现原子化操作 +- **动态配置**:支持配置中心集成 +- **灵活的键解析**:支持 `{ip}`、`{user_id}` 等占位符 + +## 安装 + +```bash +composer require friendsofhyperf/rate-limit +``` + +## 发布配置 + +```bash +php bin/hyperf.php vendor:publish friendsofhyperf/rate-limit +``` + +这将创建 `config/autoload/rate_limit.php` 配置文件。 + +## 使用 + +### 使用注解 + +```php +use FriendsOfHyperf\RateLimit\Annotation\RateLimit; + +class UserController +{ + #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: "sliding_window")] + public function index() + { + return ['message' => 'Hello World']; + } + + #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: "fixed_window")] + public function login() + { + // 登录逻辑 + } +} +``` + +### 使用中间件 + +创建一个继承 `RateLimitMiddleware` 的自定义中间件: + +```php +namespace App\Middleware; + +use FriendsOfHyperf\RateLimit\Middleware\RateLimitMiddleware; +use Psr\Http\Message\ServerRequestInterface; + +class ApiRateLimitMiddleware extends RateLimitMiddleware +{ + protected int $maxAttempts = 60; + protected int $decay = 60; + protected string $algorithm = 'sliding_window'; + + protected function resolveKey(ServerRequestInterface $request): string + { + return 'api:' . $this->getClientIp(); + } +} +``` + +然后在中间件配置中注册它。 + +### 在代码中直接使用 + +```php +use FriendsOfHyperf\RateLimit\RateLimiterFactory; + +class YourService +{ + public function __construct(private RateLimiterFactory $factory) + { + } + + public function someMethod() + { + $limiter = $this->factory->make('sliding_window'); + + $key = 'operation:user:123'; + $maxAttempts = 10; + $decay = 60; + + if ($limiter->tooManyAttempts($key, $maxAttempts, $decay)) { + throw new \Exception('超出限流次数'); + } + + // 你的业务逻辑 + } +} +``` + +## 算法说明 + +### 固定窗口(Fixed Window) + +简单的计数器,在固定时间间隔重置。速度快但可能在窗口边界出现突发流量。 + +```php +#[RateLimit(algorithm: "fixed_window", maxAttempts: 100, decay: 60)] +``` + +### 滑动窗口(Sliding Window) + +比固定窗口更精确,使用有序集合跟踪带时间戳的请求。 + +```php +#[RateLimit(algorithm: "sliding_window", maxAttempts: 100, decay: 60)] +``` + +### 令牌桶(Token Bucket) + +允许突发流量达到桶容量,令牌以恒定速率添加。 + +```php +#[RateLimit(algorithm: "token_bucket", maxAttempts: 100, decay: 60)] +``` + +### 漏桶(Leaky Bucket) + +平滑突发流量,无论到达模式如何,都以恒定速率处理请求。 + +```php +#[RateLimit(algorithm: "leaky_bucket", maxAttempts: 100, decay: 60)] +``` + +## 配置 + +配置文件支持以下选项: + +```php +return [ + 'default' => 'fixed_window', + 'connection' => 'default', + 'prefix' => 'rate_limit', + + 'defaults' => [ + 'max_attempts' => 60, + 'decay' => 60, + ], + + 'limiters' => [ + 'api' => [ + 'max_attempts' => 60, + 'decay' => 60, + 'algorithm' => 'sliding_window', + ], + 'login' => [ + 'max_attempts' => 5, + 'decay' => 60, + 'algorithm' => 'fixed_window', + ], + ], +]; +``` + +## 键占位符 + +注解支持在键中使用动态占位符: + +- `{ip}` - 客户端 IP 地址 +- `{user_id}` - 从请求属性获取的用户 ID +- 任何方法参数名称 + +示例: + +```php +#[RateLimit(key: "api:{ip}:user:{user_id}", maxAttempts: 60, decay: 60)] +public function profile($userId) +{ + // ... +} +``` + +## 许可证 + +MIT diff --git a/tests/Pest.php b/tests/Pest.php index 66121f5ee..70f9f94c5 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -35,6 +35,7 @@ uses()->group('tcp-sender')->in('TcpSender'); uses()->group('telescope')->in('Telescope'); uses()->group('lock')->in('Lock'); +uses()->group('rate-limit')->in('RateLimit'); uses()->group('validated-dto') ->beforeEach(function () { $this->subject_name = faker()->name(); diff --git a/tests/RateLimit/AnnotationTest.php b/tests/RateLimit/AnnotationTest.php new file mode 100644 index 000000000..88f3b1ab8 --- /dev/null +++ b/tests/RateLimit/AnnotationTest.php @@ -0,0 +1,46 @@ +key)->toBe(''); + expect($annotation->maxAttempts)->toBe(60); + expect($annotation->decay)->toBe(60); + expect($annotation->algorithm)->toBe('fixed_window'); + expect($annotation->response)->toBe('Too Many Attempts.'); + expect($annotation->responseCode)->toBe(429); +}); + +test('annotation accepts custom parameters', function () { + $annotation = new RateLimit( + key: 'api:{ip}', + maxAttempts: 100, + decay: 120, + algorithm: 'sliding_window', + response: 'Custom message', + responseCode: 503 + ); + + expect($annotation->key)->toBe('api:{ip}'); + expect($annotation->maxAttempts)->toBe(100); + expect($annotation->decay)->toBe(120); + expect($annotation->algorithm)->toBe('sliding_window'); + expect($annotation->response)->toBe('Custom message'); + expect($annotation->responseCode)->toBe(503); +}); + +test('annotation extends abstract annotation', function () { + $annotation = new RateLimit(); + + expect($annotation)->toBeInstanceOf(\Hyperf\Di\Annotation\AbstractAnnotation::class); +}); diff --git a/tests/RateLimit/LuaScriptsTest.php b/tests/RateLimit/LuaScriptsTest.php new file mode 100644 index 000000000..af16f0925 --- /dev/null +++ b/tests/RateLimit/LuaScriptsTest.php @@ -0,0 +1,47 @@ +toBeString(); + expect($script)->toContain('redis.call'); + expect($script)->toContain('incr'); + expect($script)->toContain('expire'); +}); + +test('sliding window script returns valid lua script', function () { + $script = LuaScripts::slidingWindow(); + + expect($script)->toBeString(); + expect($script)->toContain('zremrangebyscore'); + expect($script)->toContain('zcard'); + expect($script)->toContain('zadd'); +}); + +test('token bucket script returns valid lua script', function () { + $script = LuaScripts::tokenBucket(); + + expect($script)->toBeString(); + expect($script)->toContain('hmget'); + expect($script)->toContain('tokens'); + expect($script)->toContain('last_refill'); +}); + +test('leaky bucket script returns valid lua script', function () { + $script = LuaScripts::leakyBucket(); + + expect($script)->toBeString(); + expect($script)->toContain('hmget'); + expect($script)->toContain('water'); + expect($script)->toContain('last_leak'); +}); diff --git a/tests/RateLimit/RateLimiterFactoryTest.php b/tests/RateLimit/RateLimiterFactoryTest.php new file mode 100644 index 000000000..4d47d85f3 --- /dev/null +++ b/tests/RateLimit/RateLimiterFactoryTest.php @@ -0,0 +1,101 @@ +shouldReceive('get') + ->with(Redis::class) + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make('fixed_window'); + + expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\FixedWindowRateLimiter::class); +}); + +test('factory can create sliding window limiter', function () { + $container = m::mock(ContainerInterface::class); + $redis = m::mock(Redis::class); + + $container->shouldReceive('get') + ->with(Redis::class) + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make('sliding_window'); + + expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\SlidingWindowRateLimiter::class); +}); + +test('factory can create token bucket limiter', function () { + $container = m::mock(ContainerInterface::class); + $redis = m::mock(Redis::class); + + $container->shouldReceive('get') + ->with(Redis::class) + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make('token_bucket'); + + expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\TokenBucketRateLimiter::class); +}); + +test('factory can create leaky bucket limiter', function () { + $container = m::mock(ContainerInterface::class); + $redis = m::mock(Redis::class); + + $container->shouldReceive('get') + ->with(Redis::class) + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make('leaky_bucket'); + + expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\LeakyBucketRateLimiter::class); +}); + +test('factory throws exception for unsupported algorithm', function () { + $container = m::mock(ContainerInterface::class); + $redis = m::mock(Redis::class); + + $container->shouldReceive('get') + ->with(Redis::class) + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + + expect(fn () => $factory->make('invalid_algorithm')) + ->toThrow(RateLimitException::class); +}); + +test('factory caches limiter instances', function () { + $container = m::mock(ContainerInterface::class); + $redis = m::mock(Redis::class); + + $container->shouldReceive('get') + ->with(Redis::class) + ->once() + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter1 = $factory->make('fixed_window'); + $limiter2 = $factory->make('fixed_window'); + + expect($limiter1)->toBe($limiter2); +}); From 72444513b24e6b38788559acf03a65cc110c6f7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:18:49 +0000 Subject: [PATCH 04/39] Add usage examples for rate-limit component Co-authored-by: huangdijia <8337659+huangdijia@users.noreply.github.com> --- src/rate-limit/examples/DirectUsage.php | 58 ++++++++++++++ src/rate-limit/examples/ExampleController.php | 78 +++++++++++++++++++ src/rate-limit/examples/ExampleMiddleware.php | 34 ++++++++ 3 files changed, 170 insertions(+) create mode 100644 src/rate-limit/examples/DirectUsage.php create mode 100644 src/rate-limit/examples/ExampleController.php create mode 100644 src/rate-limit/examples/ExampleMiddleware.php diff --git a/src/rate-limit/examples/DirectUsage.php b/src/rate-limit/examples/DirectUsage.php new file mode 100644 index 000000000..018ab449f --- /dev/null +++ b/src/rate-limit/examples/DirectUsage.php @@ -0,0 +1,58 @@ +factory->make('sliding_window'); + + $key = 'user:action:' . $userId; + $maxAttempts = 10; + $decay = 60; + + if ($limiter->tooManyAttempts($key, $maxAttempts, $decay)) { + $availableIn = $limiter->availableIn($key); + throw new RateLimitException( + "Rate limit exceeded. Try again in {$availableIn} seconds." + ); + } + + // Process the user action + return [ + 'success' => true, + 'remaining' => $limiter->remaining($key, $maxAttempts), + ]; + } + + public function checkRateLimit(string $key, int $max, int $decay): array + { + $limiter = $this->factory->make('token_bucket'); + + return [ + 'attempts' => $limiter->attempts($key), + 'remaining' => $limiter->remaining($key, $max), + 'available_in' => $limiter->availableIn($key), + ]; + } +} diff --git a/src/rate-limit/examples/ExampleController.php b/src/rate-limit/examples/ExampleController.php new file mode 100644 index 000000000..6ca17c993 --- /dev/null +++ b/src/rate-limit/examples/ExampleController.php @@ -0,0 +1,78 @@ + 'API index']; + } + + /** + * Login endpoint with strict rate limiting. + */ + #[RateLimit( + key: "login:{ip}", + maxAttempts: 5, + decay: 300, + algorithm: "fixed_window", + response: "Too many login attempts. Please try again later.", + responseCode: 429 + )] + public function login() + { + return ['message' => 'Login']; + } + + /** + * User-specific rate limit using user ID. + */ + #[RateLimit( + key: "user:profile:{user_id}", + maxAttempts: 30, + decay: 60, + algorithm: "token_bucket" + )] + public function profile(int $userId) + { + return ['user_id' => $userId, 'message' => 'User profile']; + } + + /** + * Heavy operation with leaky bucket to smooth out requests. + */ + #[RateLimit( + key: "heavy:operation:{ip}", + maxAttempts: 10, + decay: 60, + algorithm: "leaky_bucket" + )] + public function heavyOperation() + { + return ['message' => 'Heavy operation completed']; + } +} diff --git a/src/rate-limit/examples/ExampleMiddleware.php b/src/rate-limit/examples/ExampleMiddleware.php new file mode 100644 index 000000000..d60fd7bda --- /dev/null +++ b/src/rate-limit/examples/ExampleMiddleware.php @@ -0,0 +1,34 @@ +getClientIp(); + } +} From ebfb282221bd9895475ba29eff538d982fb9d092 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:00:51 +0800 Subject: [PATCH 05/39] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=9A=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E7=9A=84=E9=80=9F=E7=8E=87=E9=99=90=E5=88=B6?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=E6=9C=8D=E5=8A=A1=E3=80=81=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=99=A8=E5=92=8C=E4=B8=AD=E9=97=B4=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/examples/DirectUsage.php | 58 -------------- src/rate-limit/examples/ExampleController.php | 78 ------------------- src/rate-limit/examples/ExampleMiddleware.php | 34 -------- 3 files changed, 170 deletions(-) delete mode 100644 src/rate-limit/examples/DirectUsage.php delete mode 100644 src/rate-limit/examples/ExampleController.php delete mode 100644 src/rate-limit/examples/ExampleMiddleware.php diff --git a/src/rate-limit/examples/DirectUsage.php b/src/rate-limit/examples/DirectUsage.php deleted file mode 100644 index 018ab449f..000000000 --- a/src/rate-limit/examples/DirectUsage.php +++ /dev/null @@ -1,58 +0,0 @@ -factory->make('sliding_window'); - - $key = 'user:action:' . $userId; - $maxAttempts = 10; - $decay = 60; - - if ($limiter->tooManyAttempts($key, $maxAttempts, $decay)) { - $availableIn = $limiter->availableIn($key); - throw new RateLimitException( - "Rate limit exceeded. Try again in {$availableIn} seconds." - ); - } - - // Process the user action - return [ - 'success' => true, - 'remaining' => $limiter->remaining($key, $maxAttempts), - ]; - } - - public function checkRateLimit(string $key, int $max, int $decay): array - { - $limiter = $this->factory->make('token_bucket'); - - return [ - 'attempts' => $limiter->attempts($key), - 'remaining' => $limiter->remaining($key, $max), - 'available_in' => $limiter->availableIn($key), - ]; - } -} diff --git a/src/rate-limit/examples/ExampleController.php b/src/rate-limit/examples/ExampleController.php deleted file mode 100644 index 6ca17c993..000000000 --- a/src/rate-limit/examples/ExampleController.php +++ /dev/null @@ -1,78 +0,0 @@ - 'API index']; - } - - /** - * Login endpoint with strict rate limiting. - */ - #[RateLimit( - key: "login:{ip}", - maxAttempts: 5, - decay: 300, - algorithm: "fixed_window", - response: "Too many login attempts. Please try again later.", - responseCode: 429 - )] - public function login() - { - return ['message' => 'Login']; - } - - /** - * User-specific rate limit using user ID. - */ - #[RateLimit( - key: "user:profile:{user_id}", - maxAttempts: 30, - decay: 60, - algorithm: "token_bucket" - )] - public function profile(int $userId) - { - return ['user_id' => $userId, 'message' => 'User profile']; - } - - /** - * Heavy operation with leaky bucket to smooth out requests. - */ - #[RateLimit( - key: "heavy:operation:{ip}", - maxAttempts: 10, - decay: 60, - algorithm: "leaky_bucket" - )] - public function heavyOperation() - { - return ['message' => 'Heavy operation completed']; - } -} diff --git a/src/rate-limit/examples/ExampleMiddleware.php b/src/rate-limit/examples/ExampleMiddleware.php deleted file mode 100644 index d60fd7bda..000000000 --- a/src/rate-limit/examples/ExampleMiddleware.php +++ /dev/null @@ -1,34 +0,0 @@ -getClientIp(); - } -} From 692a228ce6326b57ae7c4e20d5ef05f72cf952bd Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:03:32 +0800 Subject: [PATCH 06/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20getRedis=20=E6=96=B9=E6=B3=95=E4=BB=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20RedisFactory=20=E8=8E=B7=E5=8F=96=20Redis=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/RateLimiterFactory.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php index afa9bcee3..74653dcac 100644 --- a/src/rate-limit/src/RateLimiterFactory.php +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -18,6 +18,7 @@ use FriendsOfHyperf\RateLimit\Contract\RateLimiterInterface; use FriendsOfHyperf\RateLimit\Exception\RateLimitException; use Hyperf\Redis\Redis; +use Hyperf\Redis\RedisFactory; use Psr\Container\ContainerInterface; class RateLimiterFactory @@ -50,7 +51,7 @@ public function make(string $algorithm = 'fixed_window', ?string $connection = n protected function getRedis(?string $connection = null): Redis { - return $this->container->get(Redis::class); + return $this->container->get(RedisFactory::class)->get($connection); } protected function getPrefix(): string From 8726c642527761dbae3beff5afc6c3e1e5e56cfd Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:06:49 +0800 Subject: [PATCH 07/39] =?UTF-8?q?=E6=B8=85=E7=90=86=EF=BC=9A=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=A4=9A=E4=BD=99=E7=9A=84=E7=A9=BA=E8=A1=8C=E4=BB=A5?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/publish/rate_limit.php | 1 - .../src/Algorithm/LeakyBucketRateLimiter.php | 2 +- .../src/Algorithm/TokenBucketRateLimiter.php | 2 +- src/rate-limit/src/Aspect/RateLimitAspect.php | 16 +++++++------- .../src/Middleware/RateLimitMiddleware.php | 1 - tests/RateLimit/AnnotationTest.php | 2 +- tests/RateLimit/RateLimiterFactoryTest.php | 22 +++++++++---------- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/rate-limit/publish/rate_limit.php b/src/rate-limit/publish/rate_limit.php index 8d45a5ede..ad7d7b703 100644 --- a/src/rate-limit/publish/rate_limit.php +++ b/src/rate-limit/publish/rate_limit.php @@ -8,7 +8,6 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ - return [ /* * Default rate limiter algorithm diff --git a/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php index b6056a466..1948c78c6 100644 --- a/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php +++ b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php @@ -24,7 +24,7 @@ public function __construct(protected Redis $redis, protected string $prefix = ' public function attempt(string $key, int $maxAttempts, int $decay): bool { $leakRate = $maxAttempts / $decay; - + $result = $this->redis->eval( LuaScripts::leakyBucket(), [$this->getKey($key)], diff --git a/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php index a5d1affe1..f465646d6 100644 --- a/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php +++ b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php @@ -24,7 +24,7 @@ public function __construct(protected Redis $redis, protected string $prefix = ' public function attempt(string $key, int $maxAttempts, int $decay): bool { $refillRate = $maxAttempts / $decay; - + $result = $this->redis->eval( LuaScripts::tokenBucket(), [$this->getKey($key)], diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index f851931a2..e90a4902a 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -33,10 +33,10 @@ public function __construct( public function process(ProceedingJoinPoint $proceedingJoinPoint) { $metadata = $proceedingJoinPoint->getAnnotationMetadata(); - - /** @var RateLimit|null $annotation */ - $annotation = $metadata->method[RateLimit::class] - ?? $metadata->class[RateLimit::class] + + /** @var null|RateLimit $annotation */ + $annotation = $metadata->method[RateLimit::class] + ?? $metadata->class[RateLimit::class] ?? null; if (! $annotation) { @@ -73,16 +73,16 @@ protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPo if (str_contains($key, '{')) { $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { $placeholder = $matches[1]; - + // Try to get from request if ($placeholder === 'ip') { return $this->getClientIp(); } - + if ($placeholder === 'user_id') { return $this->getUserId(); } - + // Try to get from method arguments $arguments = $proceedingJoinPoint->arguments; foreach ($arguments['keys'] ?? [] as $argKey => $argValue) { @@ -90,7 +90,7 @@ protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPo return (string) $argValue; } } - + return $matches[0]; }, $key); } diff --git a/src/rate-limit/src/Middleware/RateLimitMiddleware.php b/src/rate-limit/src/Middleware/RateLimitMiddleware.php index fdd12c159..25344d41c 100644 --- a/src/rate-limit/src/Middleware/RateLimitMiddleware.php +++ b/src/rate-limit/src/Middleware/RateLimitMiddleware.php @@ -12,7 +12,6 @@ namespace FriendsOfHyperf\RateLimit\Middleware; use FriendsOfHyperf\RateLimit\RateLimiterFactory; -use Hyperf\Context\Context; use Hyperf\HttpServer\Contract\RequestInterface; use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse; use Psr\Container\ContainerInterface; diff --git a/tests/RateLimit/AnnotationTest.php b/tests/RateLimit/AnnotationTest.php index 88f3b1ab8..bda2198c3 100644 --- a/tests/RateLimit/AnnotationTest.php +++ b/tests/RateLimit/AnnotationTest.php @@ -42,5 +42,5 @@ test('annotation extends abstract annotation', function () { $annotation = new RateLimit(); - expect($annotation)->toBeInstanceOf(\Hyperf\Di\Annotation\AbstractAnnotation::class); + expect($annotation)->toBeInstanceOf(Hyperf\Di\Annotation\AbstractAnnotation::class); }); diff --git a/tests/RateLimit/RateLimiterFactoryTest.php b/tests/RateLimit/RateLimiterFactoryTest.php index 4d47d85f3..e81454e46 100644 --- a/tests/RateLimit/RateLimiterFactoryTest.php +++ b/tests/RateLimit/RateLimiterFactoryTest.php @@ -17,7 +17,7 @@ test('factory can create fixed window limiter', function () { $container = m::mock(ContainerInterface::class); $redis = m::mock(Redis::class); - + $container->shouldReceive('get') ->with(Redis::class) ->andReturn($redis); @@ -25,13 +25,13 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make('fixed_window'); - expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\FixedWindowRateLimiter::class); + expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\FixedWindowRateLimiter::class); }); test('factory can create sliding window limiter', function () { $container = m::mock(ContainerInterface::class); $redis = m::mock(Redis::class); - + $container->shouldReceive('get') ->with(Redis::class) ->andReturn($redis); @@ -39,13 +39,13 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make('sliding_window'); - expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\SlidingWindowRateLimiter::class); + expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\SlidingWindowRateLimiter::class); }); test('factory can create token bucket limiter', function () { $container = m::mock(ContainerInterface::class); $redis = m::mock(Redis::class); - + $container->shouldReceive('get') ->with(Redis::class) ->andReturn($redis); @@ -53,13 +53,13 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make('token_bucket'); - expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\TokenBucketRateLimiter::class); + expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\TokenBucketRateLimiter::class); }); test('factory can create leaky bucket limiter', function () { $container = m::mock(ContainerInterface::class); $redis = m::mock(Redis::class); - + $container->shouldReceive('get') ->with(Redis::class) ->andReturn($redis); @@ -67,19 +67,19 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make('leaky_bucket'); - expect($limiter)->toBeInstanceOf(\FriendsOfHyperf\RateLimit\Algorithm\LeakyBucketRateLimiter::class); + expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\LeakyBucketRateLimiter::class); }); test('factory throws exception for unsupported algorithm', function () { $container = m::mock(ContainerInterface::class); $redis = m::mock(Redis::class); - + $container->shouldReceive('get') ->with(Redis::class) ->andReturn($redis); $factory = new RateLimiterFactory($container); - + expect(fn () => $factory->make('invalid_algorithm')) ->toThrow(RateLimitException::class); }); @@ -87,7 +87,7 @@ test('factory caches limiter instances', function () { $container = m::mock(ContainerInterface::class); $redis = m::mock(Redis::class); - + $container->shouldReceive('get') ->with(Redis::class) ->once() From 4b9463d8f59b7a529e619327bf276cdbe9743359 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:09:15 +0800 Subject: [PATCH 08/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Fi?= =?UTF-8?q?xedWindowRateLimiter=20=E4=B8=AD=E7=A7=BB=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E7=9A=84=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/publish/rate_limit.php | 2 ++ src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rate-limit/publish/rate_limit.php b/src/rate-limit/publish/rate_limit.php index ad7d7b703..93ced8015 100644 --- a/src/rate-limit/publish/rate_limit.php +++ b/src/rate-limit/publish/rate_limit.php @@ -8,6 +8,8 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ +use function Hyperf\Support\env; + return [ /* * Default rate limiter algorithm diff --git a/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php b/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php index e48dc64d8..858177c46 100644 --- a/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php +++ b/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php @@ -27,7 +27,6 @@ public function attempt(string $key, int $maxAttempts, int $decay): bool LuaScripts::fixedWindow(), [$this->getKey($key)], [$maxAttempts, $decay, time()], - 1 ); return (bool) $result[0]; From 1c6a491d2b153cb5975a62851a9b9a84e7f7c417 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:10:23 +0800 Subject: [PATCH 09/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=B0=86=20Ra?= =?UTF-8?q?teLimiterFactory=20=E4=B8=AD=E7=9A=84=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=E4=BB=8E=20connection=20=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20pool=EF=BC=8C=E4=BB=A5=E6=8F=90=E9=AB=98=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/RateLimiterFactory.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php index 74653dcac..6365947f1 100644 --- a/src/rate-limit/src/RateLimiterFactory.php +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -29,15 +29,15 @@ public function __construct(protected ContainerInterface $container) { } - public function make(string $algorithm = 'fixed_window', ?string $connection = null): RateLimiterInterface + public function make(string $algorithm = 'fixed_window', ?string $pool = null): RateLimiterInterface { - $key = $algorithm . ':' . ($connection ?? 'default'); + $key = $algorithm . ':' . ($pool ?? 'default'); if (isset($this->limiters[$key])) { return $this->limiters[$key]; } - $redis = $this->getRedis($connection); + $redis = $this->getRedis($pool); $prefix = $this->getPrefix(); return $this->limiters[$key] = match ($algorithm) { @@ -49,9 +49,9 @@ public function make(string $algorithm = 'fixed_window', ?string $connection = n }; } - protected function getRedis(?string $connection = null): Redis + protected function getRedis(?string $pool = null): Redis { - return $this->container->get(RedisFactory::class)->get($connection); + return $this->container->get(RedisFactory::class)->get($pool); } protected function getPrefix(): string From 59ec6b7226a68e937b37d45316f816995a57aba1 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:12:15 +0800 Subject: [PATCH 10/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=B0=86=20Ra?= =?UTF-8?q?teLimit=20=E4=B8=AD=E7=9A=84=E7=AE=97=E6=B3=95=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E7=B1=BB=E5=9E=8B=E6=9B=B4=E6=94=B9=E4=B8=BA=20Algori?= =?UTF-8?q?thm=20=E6=9E=9A=E4=B8=BE=EF=BC=8C=E4=BB=A5=E6=8F=90=E9=AB=98?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8=E6=80=A7=EF=BC=9B=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20Algorithm=20=E6=9E=9A=E4=B8=BE=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Algorithm.php | 20 ++++++++++++++++++++ src/rate-limit/src/Annotation/RateLimit.php | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/rate-limit/src/Algorithm.php diff --git a/src/rate-limit/src/Algorithm.php b/src/rate-limit/src/Algorithm.php new file mode 100644 index 000000000..c0bcc98cc --- /dev/null +++ b/src/rate-limit/src/Algorithm.php @@ -0,0 +1,20 @@ + Date: Tue, 18 Nov 2025 08:12:51 +0800 Subject: [PATCH 11/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=B0=86=20Ra?= =?UTF-8?q?teLimiterFactory=20=E4=B8=AD=E7=9A=84=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=B1=BB=E5=9E=8B=E6=9B=B4=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20Algorithm=20=E6=9E=9A=E4=B8=BE=EF=BC=8C=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/RateLimiterFactory.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php index 6365947f1..bc48a87ca 100644 --- a/src/rate-limit/src/RateLimiterFactory.php +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -29,9 +29,9 @@ public function __construct(protected ContainerInterface $container) { } - public function make(string $algorithm = 'fixed_window', ?string $pool = null): RateLimiterInterface + public function make(Algorithm $algorithm = Algorithm::FIXED_WINDOW, ?string $pool = null): RateLimiterInterface { - $key = $algorithm . ':' . ($pool ?? 'default'); + $key = $algorithm->value . ':' . ($pool ?? 'default'); if (isset($this->limiters[$key])) { return $this->limiters[$key]; @@ -41,10 +41,10 @@ public function make(string $algorithm = 'fixed_window', ?string $pool = null): $prefix = $this->getPrefix(); return $this->limiters[$key] = match ($algorithm) { - 'fixed_window' => new FixedWindowRateLimiter($redis, $prefix), - 'sliding_window' => new SlidingWindowRateLimiter($redis, $prefix), - 'token_bucket' => new TokenBucketRateLimiter($redis, $prefix), - 'leaky_bucket' => new LeakyBucketRateLimiter($redis, $prefix), + Algorithm::FIXED_WINDOW => new FixedWindowRateLimiter($redis, $prefix), + Algorithm::SLIDING_WINDOW => new SlidingWindowRateLimiter($redis, $prefix), + Algorithm::TOKEN_BUCKET => new TokenBucketRateLimiter($redis, $prefix), + Algorithm::LEAKY_BUCKET => new LeakyBucketRateLimiter($redis, $prefix), default => throw new RateLimitException("Unsupported rate limiter algorithm: {$algorithm}"), }; } From ba1aefd78749bd1989674b71660809ac20e9d19c Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:20:43 +0800 Subject: [PATCH 12/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8?= =?UTF-8?q?=E5=90=84=E4=B8=AA=E9=80=9F=E7=8E=87=E9=99=90=E5=88=B6=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E4=B8=AD=E4=BC=98=E5=8C=96=E5=8F=82=E6=95=B0=E4=BC=A0?= =?UTF-8?q?=E9=80=92=EF=BC=8C=E5=A2=9E=E5=BC=BA=E7=B1=BB=E5=9E=8B=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=80=A7=EF=BC=9B=E6=9B=B4=E6=96=B0=20RateLimit=20?= =?UTF-8?q?=E6=B3=A8=E8=A7=A3=E4=BB=A5=E4=BD=BF=E7=94=A8=20Algorithm=20?= =?UTF-8?q?=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Algorithm/FixedWindowRateLimiter.php | 6 +-- .../src/Algorithm/LeakyBucketRateLimiter.php | 5 +-- .../Algorithm/SlidingWindowRateLimiter.php | 5 +-- .../src/Algorithm/TokenBucketRateLimiter.php | 5 +-- src/rate-limit/src/Annotation/RateLimit.php | 2 +- src/rate-limit/src/Aspect/RateLimitAspect.php | 38 ------------------- .../src/Middleware/RateLimitMiddleware.php | 19 +++++----- src/rate-limit/src/RateLimiterFactory.php | 2 - 8 files changed, 20 insertions(+), 62 deletions(-) diff --git a/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php b/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php index 858177c46..38c0c96b9 100644 --- a/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php +++ b/src/rate-limit/src/Algorithm/FixedWindowRateLimiter.php @@ -25,8 +25,8 @@ public function attempt(string $key, int $maxAttempts, int $decay): bool { $result = $this->redis->eval( LuaScripts::fixedWindow(), - [$this->getKey($key)], - [$maxAttempts, $decay, time()], + [$this->getKey($key), $maxAttempts, $decay, time()], + 1 ); return (bool) $result[0]; @@ -57,7 +57,7 @@ public function clear(string $key): void public function availableIn(string $key): int { $ttl = $this->redis->ttl($this->getKey($key)); - return $ttl > 0 ? $ttl : 0; + return (is_int($ttl) && $ttl > 0) ? $ttl : 0; } protected function getKey(string $key): string diff --git a/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php index 1948c78c6..1ed0755e5 100644 --- a/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php +++ b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php @@ -27,8 +27,7 @@ public function attempt(string $key, int $maxAttempts, int $decay): bool $result = $this->redis->eval( LuaScripts::leakyBucket(), - [$this->getKey($key)], - [$maxAttempts, $leakRate, microtime(true)], + [$this->getKey($key), $maxAttempts, $leakRate, microtime(true)], 1 ); @@ -60,7 +59,7 @@ public function clear(string $key): void public function availableIn(string $key): int { $ttl = $this->redis->ttl($this->getKey($key)); - return $ttl > 0 ? $ttl : 0; + return (is_int($ttl) && $ttl > 0) ? $ttl : 0; } protected function getKey(string $key): string diff --git a/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php b/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php index 3900768bb..c64f5ec0d 100644 --- a/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php +++ b/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php @@ -25,8 +25,7 @@ public function attempt(string $key, int $maxAttempts, int $decay): bool { $result = $this->redis->eval( LuaScripts::slidingWindow(), - [$this->getKey($key)], - [$maxAttempts, $decay, microtime(true)], + [$this->getKey($key), $maxAttempts, $decay, microtime(true)], 1 ); @@ -57,7 +56,7 @@ public function clear(string $key): void public function availableIn(string $key): int { $ttl = $this->redis->ttl($this->getKey($key)); - return $ttl > 0 ? $ttl : 0; + return (is_int($ttl) && $ttl > 0) ? $ttl : 0; } protected function getKey(string $key): string diff --git a/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php index f465646d6..1569633bd 100644 --- a/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php +++ b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php @@ -27,8 +27,7 @@ public function attempt(string $key, int $maxAttempts, int $decay): bool $result = $this->redis->eval( LuaScripts::tokenBucket(), - [$this->getKey($key)], - [$maxAttempts, $refillRate, 1, microtime(true)], + [$this->getKey($key), $maxAttempts, $refillRate, 1, microtime(true)], 1 ); @@ -60,7 +59,7 @@ public function clear(string $key): void public function availableIn(string $key): int { $ttl = $this->redis->ttl($this->getKey($key)); - return $ttl > 0 ? $ttl : 0; + return (is_int($ttl) && $ttl > 0) ? $ttl : 0; } protected function getKey(string $key): string diff --git a/src/rate-limit/src/Annotation/RateLimit.php b/src/rate-limit/src/Annotation/RateLimit.php index f0321f00a..98c3e771c 100644 --- a/src/rate-limit/src/Annotation/RateLimit.php +++ b/src/rate-limit/src/Annotation/RateLimit.php @@ -22,7 +22,7 @@ class RateLimit extends AbstractAnnotation * @param string $key Rate limit key, supports placeholders like {user_id} * @param int $maxAttempts Maximum number of attempts allowed * @param int $decay Time window in seconds - * @param string $algorithm Algorithm to use: fixed_window, sliding_window, token_bucket, leaky_bucket + * @param Algorithm $algorithm Algorithm to use: fixed_window, sliding_window, token_bucket, leaky_bucket * @param string $response Custom response when rate limit is exceeded * @param int $responseCode HTTP response code when rate limit is exceeded */ diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index e90a4902a..01b1655a8 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -16,7 +16,6 @@ use FriendsOfHyperf\RateLimit\RateLimiterFactory; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; -use Hyperf\HttpServer\Contract\RequestInterface; class RateLimitAspect extends AbstractAspect { @@ -26,7 +25,6 @@ class RateLimitAspect extends AbstractAspect public function __construct( protected RateLimiterFactory $factory, - protected RequestInterface $request ) { } @@ -74,15 +72,6 @@ protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPo $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { $placeholder = $matches[1]; - // Try to get from request - if ($placeholder === 'ip') { - return $this->getClientIp(); - } - - if ($placeholder === 'user_id') { - return $this->getUserId(); - } - // Try to get from method arguments $arguments = $proceedingJoinPoint->arguments; foreach ($arguments['keys'] ?? [] as $argKey => $argValue) { @@ -97,31 +86,4 @@ protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPo return $key; } - - protected function getClientIp(): string - { - $headers = [ - 'x-forwarded-for', - 'x-real-ip', - 'remote-addr', - ]; - - foreach ($headers as $header) { - if ($ip = $this->request->getHeaderLine($header)) { - // Get first IP if comma-separated list - if (str_contains($ip, ',')) { - $ip = trim(explode(',', $ip)[0]); - } - return $ip; - } - } - - return $this->request->server('remote_addr', 'unknown'); - } - - protected function getUserId(): string - { - // This is a placeholder - should be customized based on your auth system - return (string) ($this->request->getAttribute('user_id') ?? 'guest'); - } } diff --git a/src/rate-limit/src/Middleware/RateLimitMiddleware.php b/src/rate-limit/src/Middleware/RateLimitMiddleware.php index 25344d41c..244fa0ba9 100644 --- a/src/rate-limit/src/Middleware/RateLimitMiddleware.php +++ b/src/rate-limit/src/Middleware/RateLimitMiddleware.php @@ -11,6 +11,7 @@ namespace FriendsOfHyperf\RateLimit\Middleware; +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\RateLimiterFactory; use Hyperf\HttpServer\Contract\RequestInterface; use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse; @@ -35,7 +36,7 @@ abstract class RateLimitMiddleware implements MiddlewareInterface /** * Algorithm to use: fixed_window, sliding_window, token_bucket, leaky_bucket. */ - protected string $algorithm = 'fixed_window'; + protected Algorithm $algorithm = Algorithm::FIXED_WINDOW; /** * Response message when rate limit exceeded. @@ -113,15 +114,15 @@ protected function getClientIp(): string protected function buildRateLimitExceededResponse(string $key, int $retryAfter): ResponseInterface { return $this->response + ->json([ + 'message' => $this->responseMessage, + 'retry_after' => $retryAfter, + ]) ->withStatus($this->responseCode) ->withAddedHeader('Retry-After', (string) $retryAfter) ->withAddedHeader('X-RateLimit-Limit', (string) $this->maxAttempts) ->withAddedHeader('X-RateLimit-Remaining', '0') - ->withAddedHeader('X-RateLimit-Reset', (string) (time() + $retryAfter)) - ->json([ - 'message' => $this->responseMessage, - 'retry_after' => $retryAfter, - ]); + ->withAddedHeader('X-RateLimit-Reset', (string) (time() + $retryAfter)); } /** @@ -134,8 +135,8 @@ protected function addHeaders( int $retryAfter ): ResponseInterface { return $response - ->withAddedHeader('X-RateLimit-Limit', (string) $maxAttempts) - ->withAddedHeader('X-RateLimit-Remaining', (string) max(0, $remaining)) - ->withAddedHeader('X-RateLimit-Reset', (string) (time() + $retryAfter)); + ->withHeader('X-RateLimit-Limit', (string) $maxAttempts) + ->withHeader('X-RateLimit-Remaining', (string) max(0, $remaining)) + ->withHeader('X-RateLimit-Reset', (string) (time() + $retryAfter)); } } diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php index bc48a87ca..b19be959a 100644 --- a/src/rate-limit/src/RateLimiterFactory.php +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -16,7 +16,6 @@ use FriendsOfHyperf\RateLimit\Algorithm\SlidingWindowRateLimiter; use FriendsOfHyperf\RateLimit\Algorithm\TokenBucketRateLimiter; use FriendsOfHyperf\RateLimit\Contract\RateLimiterInterface; -use FriendsOfHyperf\RateLimit\Exception\RateLimitException; use Hyperf\Redis\Redis; use Hyperf\Redis\RedisFactory; use Psr\Container\ContainerInterface; @@ -45,7 +44,6 @@ public function make(Algorithm $algorithm = Algorithm::FIXED_WINDOW, ?string $po Algorithm::SLIDING_WINDOW => new SlidingWindowRateLimiter($redis, $prefix), Algorithm::TOKEN_BUCKET => new TokenBucketRateLimiter($redis, $prefix), Algorithm::LEAKY_BUCKET => new LeakyBucketRateLimiter($redis, $prefix), - default => throw new RateLimitException("Unsupported rate limiter algorithm: {$algorithm}"), }; } From 4f911c2ee78f5ddc581215765939b8c260236689 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:33:05 +0800 Subject: [PATCH 13/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20RateLimit=20=E6=B3=A8=E8=A7=A3=E4=B8=AD=E7=9A=84=20?= =?UTF-8?q?key=20=E5=8F=82=E6=95=B0=E7=B1=BB=E5=9E=8B=E4=B8=BA=20string|ar?= =?UTF-8?q?ray=EF=BC=8C=E4=BB=A5=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E7=81=B5=E6=B4=BB=E6=80=A7=EF=BC=9B=E8=B0=83=E6=95=B4=20RateLi?= =?UTF-8?q?mitAspect=20=E4=B8=AD=E7=9A=84=20resolveKey=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BB=A5=E5=8C=B9=E9=85=8D=E6=96=B0=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/publish/rate_limit.php | 60 ------------------- src/rate-limit/src/Annotation/RateLimit.php | 4 +- src/rate-limit/src/Aspect/RateLimitAspect.php | 6 +- 3 files changed, 7 insertions(+), 63 deletions(-) delete mode 100644 src/rate-limit/publish/rate_limit.php diff --git a/src/rate-limit/publish/rate_limit.php b/src/rate-limit/publish/rate_limit.php deleted file mode 100644 index 93ced8015..000000000 --- a/src/rate-limit/publish/rate_limit.php +++ /dev/null @@ -1,60 +0,0 @@ - env('RATE_LIMIT_ALGORITHM', 'fixed_window'), - - /* - * Redis connection name - * Uses the default Redis connection if not specified - */ - 'connection' => env('RATE_LIMIT_REDIS_CONNECTION', 'default'), - - /* - * Prefix for rate limit keys in Redis - */ - 'prefix' => env('RATE_LIMIT_PREFIX', 'rate_limit'), - - /* - * Default rate limit settings - */ - 'defaults' => [ - 'max_attempts' => 60, - 'decay' => 60, // seconds - ], - - /* - * Named rate limiters - * You can define custom rate limiters here - */ - 'limiters' => [ - 'api' => [ - 'max_attempts' => 60, - 'decay' => 60, - 'algorithm' => 'sliding_window', - ], - 'login' => [ - 'max_attempts' => 5, - 'decay' => 60, - 'algorithm' => 'fixed_window', - ], - 'global' => [ - 'max_attempts' => 1000, - 'decay' => 60, - 'algorithm' => 'token_bucket', - ], - ], -]; diff --git a/src/rate-limit/src/Annotation/RateLimit.php b/src/rate-limit/src/Annotation/RateLimit.php index 98c3e771c..1d2271430 100644 --- a/src/rate-limit/src/Annotation/RateLimit.php +++ b/src/rate-limit/src/Annotation/RateLimit.php @@ -19,7 +19,7 @@ class RateLimit extends AbstractAnnotation { /** - * @param string $key Rate limit key, supports placeholders like {user_id} + * @param string|array $key Rate limit key, supports placeholders like {user_id} * @param int $maxAttempts Maximum number of attempts allowed * @param int $decay Time window in seconds * @param Algorithm $algorithm Algorithm to use: fixed_window, sliding_window, token_bucket, leaky_bucket @@ -27,7 +27,7 @@ class RateLimit extends AbstractAnnotation * @param int $responseCode HTTP response code when rate limit is exceeded */ public function __construct( - public string $key = '', + public string|array $key = '', public int $maxAttempts = 60, public int $decay = 60, public Algorithm $algorithm = Algorithm::FIXED_WINDOW, diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 01b1655a8..6e11ac0c0 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -58,7 +58,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) return $proceedingJoinPoint->process(); } - protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPoint): string + protected function resolveKey(string|array $key, ProceedingJoinPoint $proceedingJoinPoint): string { if (empty($key)) { // Use method signature as default key @@ -67,6 +67,10 @@ protected function resolveKey(string $key, ProceedingJoinPoint $proceedingJoinPo $key = "{$className}:{$methodName}"; } + if (is_callable($key)) { + return $key($proceedingJoinPoint); + } + // Support placeholders like {user_id}, {ip}, etc. if (str_contains($key, '{')) { $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { From a9bc464493cf18f84130160b5ec061492125489b Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:40:48 +0800 Subject: [PATCH 14/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BB=A5=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E9=80=9F=E7=8E=87=E9=99=90=E5=88=B6=E5=BC=82=E5=B8=B8?= =?UTF-8?q?=E7=9A=84=E5=8F=AF=E5=AE=9A=E5=88=B6=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 6e11ac0c0..df7207987 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -51,7 +51,8 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) '%s Please try again in %d seconds.', $annotation->response, $availableIn - ) + ), + $annotation->responseCode ); } From 00626c7d45ab9bc9b0b0cb3a0d6ebe7442885a1e Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:41:15 +0800 Subject: [PATCH 15/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20RateLimitAspect=20=E4=B8=AD=E7=9A=84=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E8=8E=B7=E5=8F=96=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=AF=B9=E7=B1=BB=E6=B3=A8=E8=A7=A3=E7=9A=84=E5=86=97?= =?UTF-8?q?=E4=BD=99=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index df7207987..437848e63 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -33,9 +33,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) $metadata = $proceedingJoinPoint->getAnnotationMetadata(); /** @var null|RateLimit $annotation */ - $annotation = $metadata->method[RateLimit::class] - ?? $metadata->class[RateLimit::class] - ?? null; + $annotation = $metadata->method[RateLimit::class] ?? null; if (! $annotation) { return $proceedingJoinPoint->process(); From 4fb89f84cdf8a9895efe982accdb476fa433d20d Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:41:35 +0800 Subject: [PATCH 16/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20RateLimit=20=E6=B3=A8=E8=A7=A3=E7=9A=84=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E7=9B=AE=E6=A0=87=EF=BC=8C=E4=BB=85=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=BA=A7=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Annotation/RateLimit.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit/src/Annotation/RateLimit.php b/src/rate-limit/src/Annotation/RateLimit.php index 1d2271430..555e7d70e 100644 --- a/src/rate-limit/src/Annotation/RateLimit.php +++ b/src/rate-limit/src/Annotation/RateLimit.php @@ -15,7 +15,7 @@ use FriendsOfHyperf\RateLimit\Algorithm; use Hyperf\Di\Annotation\AbstractAnnotation; -#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)] +#[Attribute(Attribute::TARGET_METHOD)] class RateLimit extends AbstractAnnotation { /** From 10e05e8d9ebd3e156bf4fb151a7c147ebfff5fa7 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:47:22 +0800 Subject: [PATCH 17/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimiterFactory=20=E4=B8=AD=E9=BB=98=E8=AE=A4=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20'default'=20Redis=20=E6=B1=A0=EF=BC=9B=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E4=BB=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20Algorithm=20=E6=9E=9A=E4=B8=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/RateLimiterFactory.php | 2 +- tests/RateLimit/AnnotationTest.php | 7 ++- tests/RateLimit/RateLimiterFactoryTest.php | 72 ++++++++++++---------- 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php index b19be959a..2c8963a74 100644 --- a/src/rate-limit/src/RateLimiterFactory.php +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -49,7 +49,7 @@ public function make(Algorithm $algorithm = Algorithm::FIXED_WINDOW, ?string $po protected function getRedis(?string $pool = null): Redis { - return $this->container->get(RedisFactory::class)->get($pool); + return $this->container->get(RedisFactory::class)->get($pool ?? 'default'); } protected function getPrefix(): string diff --git a/tests/RateLimit/AnnotationTest.php b/tests/RateLimit/AnnotationTest.php index bda2198c3..f0eaffbe2 100644 --- a/tests/RateLimit/AnnotationTest.php +++ b/tests/RateLimit/AnnotationTest.php @@ -8,6 +8,7 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\Annotation\RateLimit; test('annotation can be instantiated with defaults', function () { @@ -16,7 +17,7 @@ expect($annotation->key)->toBe(''); expect($annotation->maxAttempts)->toBe(60); expect($annotation->decay)->toBe(60); - expect($annotation->algorithm)->toBe('fixed_window'); + expect($annotation->algorithm)->toBe(Algorithm::FIXED_WINDOW); expect($annotation->response)->toBe('Too Many Attempts.'); expect($annotation->responseCode)->toBe(429); }); @@ -26,7 +27,7 @@ key: 'api:{ip}', maxAttempts: 100, decay: 120, - algorithm: 'sliding_window', + algorithm: Algorithm::SLIDING_WINDOW, response: 'Custom message', responseCode: 503 ); @@ -34,7 +35,7 @@ expect($annotation->key)->toBe('api:{ip}'); expect($annotation->maxAttempts)->toBe(100); expect($annotation->decay)->toBe(120); - expect($annotation->algorithm)->toBe('sliding_window'); + expect($annotation->algorithm)->toBe(Algorithm::SLIDING_WINDOW); expect($annotation->response)->toBe('Custom message'); expect($annotation->responseCode)->toBe(503); }); diff --git a/tests/RateLimit/RateLimiterFactoryTest.php b/tests/RateLimit/RateLimiterFactoryTest.php index e81454e46..713307b72 100644 --- a/tests/RateLimit/RateLimiterFactoryTest.php +++ b/tests/RateLimit/RateLimiterFactoryTest.php @@ -8,94 +8,102 @@ * @document https://github.com/friendsofhyperf/components/blob/main/README.md * @contact huangdijia@gmail.com */ -use FriendsOfHyperf\RateLimit\Exception\RateLimitException; +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\RateLimiterFactory; -use Hyperf\Redis\Redis; +use Hyperf\Redis\RedisProxy; +use Hyperf\Redis\RedisFactory; use Mockery as m; use Psr\Container\ContainerInterface; test('factory can create fixed window limiter', function () { $container = m::mock(ContainerInterface::class); - $redis = m::mock(Redis::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); $container->shouldReceive('get') - ->with(Redis::class) + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') ->andReturn($redis); $factory = new RateLimiterFactory($container); - $limiter = $factory->make('fixed_window'); + $limiter = $factory->make(Algorithm::FIXED_WINDOW); expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\FixedWindowRateLimiter::class); }); test('factory can create sliding window limiter', function () { $container = m::mock(ContainerInterface::class); - $redis = m::mock(Redis::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); $container->shouldReceive('get') - ->with(Redis::class) + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') ->andReturn($redis); $factory = new RateLimiterFactory($container); - $limiter = $factory->make('sliding_window'); + $limiter = $factory->make(Algorithm::SLIDING_WINDOW); expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\SlidingWindowRateLimiter::class); }); test('factory can create token bucket limiter', function () { $container = m::mock(ContainerInterface::class); - $redis = m::mock(Redis::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); $container->shouldReceive('get') - ->with(Redis::class) + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') ->andReturn($redis); $factory = new RateLimiterFactory($container); - $limiter = $factory->make('token_bucket'); + $limiter = $factory->make(Algorithm::TOKEN_BUCKET); expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\TokenBucketRateLimiter::class); }); test('factory can create leaky bucket limiter', function () { $container = m::mock(ContainerInterface::class); - $redis = m::mock(Redis::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); $container->shouldReceive('get') - ->with(Redis::class) + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') ->andReturn($redis); $factory = new RateLimiterFactory($container); - $limiter = $factory->make('leaky_bucket'); + $limiter = $factory->make(Algorithm::LEAKY_BUCKET); expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\LeakyBucketRateLimiter::class); }); -test('factory throws exception for unsupported algorithm', function () { - $container = m::mock(ContainerInterface::class); - $redis = m::mock(Redis::class); - - $container->shouldReceive('get') - ->with(Redis::class) - ->andReturn($redis); - - $factory = new RateLimiterFactory($container); - - expect(fn () => $factory->make('invalid_algorithm')) - ->toThrow(RateLimitException::class); -}); - test('factory caches limiter instances', function () { $container = m::mock(ContainerInterface::class); - $redis = m::mock(Redis::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); $container->shouldReceive('get') - ->with(Redis::class) + ->with(RedisFactory::class) + ->once() + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') ->once() ->andReturn($redis); $factory = new RateLimiterFactory($container); - $limiter1 = $factory->make('fixed_window'); - $limiter2 = $factory->make('fixed_window'); + $limiter1 = $factory->make(Algorithm::FIXED_WINDOW); + $limiter2 = $factory->make(Algorithm::FIXED_WINDOW); expect($limiter1)->toBe($limiter2); }); From b578553390c755527dea4fbae70c8d0e91236ba9 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:48:55 +0800 Subject: [PATCH 18/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimit=20=E6=B3=A8=E8=A7=A3=E4=B8=AD=E6=B7=BB=E5=8A=A0=20Redis?= =?UTF-8?q?=20=E8=BF=9E=E6=8E=A5=E6=B1=A0=E5=8F=82=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=20RateLimitAspect=20=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=AF=A5=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Annotation/RateLimit.php | 2 ++ src/rate-limit/src/Aspect/RateLimitAspect.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rate-limit/src/Annotation/RateLimit.php b/src/rate-limit/src/Annotation/RateLimit.php index 555e7d70e..b55c74ada 100644 --- a/src/rate-limit/src/Annotation/RateLimit.php +++ b/src/rate-limit/src/Annotation/RateLimit.php @@ -23,6 +23,7 @@ class RateLimit extends AbstractAnnotation * @param int $maxAttempts Maximum number of attempts allowed * @param int $decay Time window in seconds * @param Algorithm $algorithm Algorithm to use: fixed_window, sliding_window, token_bucket, leaky_bucket + * @param null|string $pool The Redis connection pool to use * @param string $response Custom response when rate limit is exceeded * @param int $responseCode HTTP response code when rate limit is exceeded */ @@ -31,6 +32,7 @@ public function __construct( public int $maxAttempts = 60, public int $decay = 60, public Algorithm $algorithm = Algorithm::FIXED_WINDOW, + public ?string $pool = null, public string $response = 'Too Many Attempts.', public int $responseCode = 429 ) { diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 437848e63..87a437d57 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -40,7 +40,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) } $key = $this->resolveKey($annotation->key, $proceedingJoinPoint); - $limiter = $this->factory->make($annotation->algorithm); + $limiter = $this->factory->make($annotation->algorithm, $annotation->pool); if ($limiter->tooManyAttempts($key, $annotation->maxAttempts, $annotation->decay)) { $availableIn = $limiter->availableIn($key); From df36a1a224bfdc07a8e7c73abdc6721b3f1befaa Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:49:34 +0800 Subject: [PATCH 19/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitMiddleware=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=20Redis=20?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=B1=A0=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E8=87=AA=E5=AE=9A=E4=B9=89=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=B1=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Middleware/RateLimitMiddleware.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/rate-limit/src/Middleware/RateLimitMiddleware.php b/src/rate-limit/src/Middleware/RateLimitMiddleware.php index 244fa0ba9..8d0ac92a1 100644 --- a/src/rate-limit/src/Middleware/RateLimitMiddleware.php +++ b/src/rate-limit/src/Middleware/RateLimitMiddleware.php @@ -38,6 +38,11 @@ abstract class RateLimitMiddleware implements MiddlewareInterface */ protected Algorithm $algorithm = Algorithm::FIXED_WINDOW; + /** + * Redis connection pool to use. + */ + protected ?string $pool = null; + /** * Response message when rate limit exceeded. */ @@ -59,7 +64,7 @@ public function __construct( public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $key = $this->resolveKey($request); - $limiter = $this->factory->make($this->algorithm); + $limiter = $this->factory->make($this->algorithm, $this->pool); if ($limiter->tooManyAttempts($key, $this->maxAttempts, $this->decay)) { return $this->buildRateLimitExceededResponse($key, $limiter->availableIn($key)); From b22610c00d2405bb4bfb404470ee7388873e3c72 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:50:13 +0800 Subject: [PATCH 20/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20RateLimiterFactoryTest=20=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E7=B1=BB=E5=BC=95=E7=94=A8=EF=BC=8C=E7=AE=80=E5=8C=96=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E7=B1=BB=E7=9A=84=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/RateLimit/RateLimiterFactoryTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/RateLimit/RateLimiterFactoryTest.php b/tests/RateLimit/RateLimiterFactoryTest.php index 713307b72..d11ac7624 100644 --- a/tests/RateLimit/RateLimiterFactoryTest.php +++ b/tests/RateLimit/RateLimiterFactoryTest.php @@ -10,8 +10,8 @@ */ use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\RateLimiterFactory; -use Hyperf\Redis\RedisProxy; use Hyperf\Redis\RedisFactory; +use Hyperf\Redis\RedisProxy; use Mockery as m; use Psr\Container\ContainerInterface; @@ -30,7 +30,7 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make(Algorithm::FIXED_WINDOW); - expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\FixedWindowRateLimiter::class); + expect($limiter)->toBeInstanceOf(Algorithm\FixedWindowRateLimiter::class); }); test('factory can create sliding window limiter', function () { @@ -48,7 +48,7 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make(Algorithm::SLIDING_WINDOW); - expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\SlidingWindowRateLimiter::class); + expect($limiter)->toBeInstanceOf(Algorithm\SlidingWindowRateLimiter::class); }); test('factory can create token bucket limiter', function () { @@ -66,7 +66,7 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make(Algorithm::TOKEN_BUCKET); - expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\TokenBucketRateLimiter::class); + expect($limiter)->toBeInstanceOf(Algorithm\TokenBucketRateLimiter::class); }); test('factory can create leaky bucket limiter', function () { @@ -84,7 +84,7 @@ $factory = new RateLimiterFactory($container); $limiter = $factory->make(Algorithm::LEAKY_BUCKET); - expect($limiter)->toBeInstanceOf(FriendsOfHyperf\RateLimit\Algorithm\LeakyBucketRateLimiter::class); + expect($limiter)->toBeInstanceOf(Algorithm\LeakyBucketRateLimiter::class); }); test('factory caches limiter instances', function () { From 0f34d64a3cfbcb590fcc5825c761262724874447 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:53:27 +0800 Subject: [PATCH 21/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20co?= =?UTF-8?q?mposer.json=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=20rate-limit=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=BB=A5=E5=8C=85=E5=90=AB=20closure-job=20=E5=92=8C=20rate-li?= =?UTF-8?q?mit=20=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=B7=BB=E5=8A=A0=20GitHub=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=92=8C=E8=AE=B8=E5=8F=AF=E8=AF=81?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 6 +++--- docs/zh-cn/guide/start/components.md | 2 ++ src/.github/profile/README.md | 2 ++ src/closure-job/.gitattributes | 4 ++++ src/closure-job/.github/FUNDING.yml | 2 ++ .../.github/workflows/close-pull-request.yml | 9 ++++++++ .../.github/workflows/release.yaml | 10 +++++++++ src/closure-job/LICENSE | 21 +++++++++++++++++++ src/rate-limit/.github/FUNDING.yml | 2 ++ .../.github/workflows/close-pull-request.yml | 9 ++++++++ src/rate-limit/.github/workflows/release.yaml | 10 +++++++++ 11 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/closure-job/.gitattributes create mode 100644 src/closure-job/.github/FUNDING.yml create mode 100644 src/closure-job/.github/workflows/close-pull-request.yml create mode 100644 src/closure-job/.github/workflows/release.yaml create mode 100644 src/closure-job/LICENSE create mode 100644 src/rate-limit/.github/FUNDING.yml create mode 100644 src/rate-limit/.github/workflows/close-pull-request.yml create mode 100644 src/rate-limit/.github/workflows/release.yaml diff --git a/composer.json b/composer.json index e52319257..ea88bcd63 100644 --- a/composer.json +++ b/composer.json @@ -138,7 +138,6 @@ "friendsofhyperf/ipc-broadcaster": "*", "friendsofhyperf/lock": "*", "friendsofhyperf/macros": "*", - "friendsofhyperf/rate-limit": "*", "friendsofhyperf/mail": "*", "friendsofhyperf/model-factory": "*", "friendsofhyperf/model-hashids": "*", @@ -154,6 +153,7 @@ "friendsofhyperf/openai-client": "*", "friendsofhyperf/pretty-console": "*", "friendsofhyperf/purifier": "*", + "friendsofhyperf/rate-limit": "*", "friendsofhyperf/recaptcha": "*", "friendsofhyperf/redis-subscriber": "*", "friendsofhyperf/sentry": "*", @@ -193,7 +193,6 @@ "FriendsOfHyperf\\Lock\\": "src/lock/src/", "FriendsOfHyperf\\Macros\\": "src/macros/src/", "FriendsOfHyperf\\Mail\\": "src/mail/src/", - "FriendsOfHyperf\\RateLimit\\": "src/rate-limit/src/", "FriendsOfHyperf\\ModelFactory\\": "src/model-factory/src/", "FriendsOfHyperf\\ModelHashids\\": "src/model-hashids/src/", "FriendsOfHyperf\\ModelMorphAddon\\": "src/model-morph-addon/src/", @@ -208,6 +207,7 @@ "FriendsOfHyperf\\OpenAi\\": "src/openai-client/src/", "FriendsOfHyperf\\PrettyConsole\\": "src/pretty-console/src/", "FriendsOfHyperf\\Purifier\\": "src/purifier/src/", + "FriendsOfHyperf\\RateLimit\\": "src/rate-limit/src/", "FriendsOfHyperf\\ReCaptcha\\": "src/recaptcha/src/", "FriendsOfHyperf\\Redis\\Subscriber\\": "src/redis-subscriber/src/", "FriendsOfHyperf\\Sentry\\": "src/sentry/src/", @@ -274,7 +274,6 @@ "FriendsOfHyperf\\Lock\\ConfigProvider", "FriendsOfHyperf\\Macros\\ConfigProvider", "FriendsOfHyperf\\Mail\\ConfigProvider", - "FriendsOfHyperf\\RateLimit\\ConfigProvider", "FriendsOfHyperf\\ModelFactory\\ConfigProvider", "FriendsOfHyperf\\ModelHashids\\ConfigProvider", "FriendsOfHyperf\\ModelMorphAddon\\ConfigProvider", @@ -289,6 +288,7 @@ "FriendsOfHyperf\\OpenAi\\ConfigProvider", "FriendsOfHyperf\\PrettyConsole\\ConfigProvider", "FriendsOfHyperf\\Purifier\\ConfigProvider", + "FriendsOfHyperf\\RateLimit\\ConfigProvider", "FriendsOfHyperf\\ReCaptcha\\ConfigProvider", "FriendsOfHyperf\\Sentry\\ConfigProvider", "FriendsOfHyperf\\Support\\ConfigProvider", diff --git a/docs/zh-cn/guide/start/components.md b/docs/zh-cn/guide/start/components.md index f61581e45..9f782ed37 100644 --- a/docs/zh-cn/guide/start/components.md +++ b/docs/zh-cn/guide/start/components.md @@ -6,6 +6,7 @@ |--|--|--|--| |[amqp-job](https://github.com/friendsofhyperf/amqp-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/amqp-job/v)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/downloads)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/amqp-job)| |[cache](https://github.com/friendsofhyperf/cache)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/cache/v)](https://packagist.org/packages/friendsofhyperf/cache)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/cache/downloads)](https://packagist.org/packages/friendsofhyperf/cache)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/cache/d/monthly)](https://packagist.org/packages/friendsofhyperf/cache)| +|[closure-job](https://github.com/friendsofhyperf/closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/closure-job/v)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/closure-job)| |[co-phpunit](https://github.com/friendsofhyperf/co-phpunit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/co-phpunit/v)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/downloads)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/d/monthly)](https://packagist.org/packages/friendsofhyperf/co-phpunit)| |[command-benchmark](https://github.com/friendsofhyperf/command-benchmark)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-benchmark/v)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/downloads)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-benchmark)| |[command-signals](https://github.com/friendsofhyperf/command-signals)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-signals/v)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/downloads)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-signals)| @@ -41,6 +42,7 @@ |[openai-client](https://github.com/friendsofhyperf/openai-client)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/openai-client/v)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/downloads)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/d/monthly)](https://packagist.org/packages/friendsofhyperf/openai-client)| |[pretty-console](https://github.com/friendsofhyperf/pretty-console)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/pretty-console/v)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/downloads)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/d/monthly)](https://packagist.org/packages/friendsofhyperf/pretty-console)| |[purifier](https://github.com/friendsofhyperf/purifier)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/purifier/v)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/purifier/downloads)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/purifier/d/monthly)](https://packagist.org/packages/friendsofhyperf/purifier)| +|[rate-limit](https://github.com/friendsofhyperf/rate-limit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/rate-limit/v)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/downloads)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/d/monthly)](https://packagist.org/packages/friendsofhyperf/rate-limit)| |[recaptcha](https://github.com/friendsofhyperf/recaptcha)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/recaptcha/v)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/downloads)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/d/monthly)](https://packagist.org/packages/friendsofhyperf/recaptcha)| |[redis-subscriber](https://github.com/friendsofhyperf/redis-subscriber)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/redis-subscriber/v)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/downloads)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/d/monthly)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)| |[sentry](https://github.com/friendsofhyperf/sentry)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/sentry/v)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/sentry/downloads)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/sentry/d/monthly)](https://packagist.org/packages/friendsofhyperf/sentry)| diff --git a/src/.github/profile/README.md b/src/.github/profile/README.md index f61581e45..9f782ed37 100644 --- a/src/.github/profile/README.md +++ b/src/.github/profile/README.md @@ -6,6 +6,7 @@ |--|--|--|--| |[amqp-job](https://github.com/friendsofhyperf/amqp-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/amqp-job/v)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/downloads)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/amqp-job)| |[cache](https://github.com/friendsofhyperf/cache)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/cache/v)](https://packagist.org/packages/friendsofhyperf/cache)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/cache/downloads)](https://packagist.org/packages/friendsofhyperf/cache)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/cache/d/monthly)](https://packagist.org/packages/friendsofhyperf/cache)| +|[closure-job](https://github.com/friendsofhyperf/closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/closure-job/v)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/closure-job)| |[co-phpunit](https://github.com/friendsofhyperf/co-phpunit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/co-phpunit/v)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/downloads)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/d/monthly)](https://packagist.org/packages/friendsofhyperf/co-phpunit)| |[command-benchmark](https://github.com/friendsofhyperf/command-benchmark)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-benchmark/v)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/downloads)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-benchmark)| |[command-signals](https://github.com/friendsofhyperf/command-signals)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-signals/v)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/downloads)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-signals)| @@ -41,6 +42,7 @@ |[openai-client](https://github.com/friendsofhyperf/openai-client)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/openai-client/v)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/downloads)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/d/monthly)](https://packagist.org/packages/friendsofhyperf/openai-client)| |[pretty-console](https://github.com/friendsofhyperf/pretty-console)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/pretty-console/v)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/downloads)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/d/monthly)](https://packagist.org/packages/friendsofhyperf/pretty-console)| |[purifier](https://github.com/friendsofhyperf/purifier)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/purifier/v)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/purifier/downloads)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/purifier/d/monthly)](https://packagist.org/packages/friendsofhyperf/purifier)| +|[rate-limit](https://github.com/friendsofhyperf/rate-limit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/rate-limit/v)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/downloads)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/d/monthly)](https://packagist.org/packages/friendsofhyperf/rate-limit)| |[recaptcha](https://github.com/friendsofhyperf/recaptcha)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/recaptcha/v)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/downloads)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/d/monthly)](https://packagist.org/packages/friendsofhyperf/recaptcha)| |[redis-subscriber](https://github.com/friendsofhyperf/redis-subscriber)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/redis-subscriber/v)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/downloads)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/d/monthly)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)| |[sentry](https://github.com/friendsofhyperf/sentry)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/sentry/v)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/sentry/downloads)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/sentry/d/monthly)](https://packagist.org/packages/friendsofhyperf/sentry)| diff --git a/src/closure-job/.gitattributes b/src/closure-job/.gitattributes new file mode 100644 index 000000000..c90148b0e --- /dev/null +++ b/src/closure-job/.gitattributes @@ -0,0 +1,4 @@ +/.github export-ignore +/.vscode export-ignore +/tests export-ignore +.gitattributes export-ignore \ No newline at end of file diff --git a/src/closure-job/.github/FUNDING.yml b/src/closure-job/.github/FUNDING.yml new file mode 100644 index 000000000..e52b3cbcd --- /dev/null +++ b/src/closure-job/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: huangdijia +custom: https://hdj.me/sponsors/ \ No newline at end of file diff --git a/src/closure-job/.github/workflows/close-pull-request.yml b/src/closure-job/.github/workflows/close-pull-request.yml new file mode 100644 index 000000000..8605acc19 --- /dev/null +++ b/src/closure-job/.github/workflows/close-pull-request.yml @@ -0,0 +1,9 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + uses: friendsofhyperf/.github/.github/workflows/close-pull-request.yml@main diff --git a/src/closure-job/.github/workflows/release.yaml b/src/closure-job/.github/workflows/release.yaml new file mode 100644 index 000000000..f924464bf --- /dev/null +++ b/src/closure-job/.github/workflows/release.yaml @@ -0,0 +1,10 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + uses: friendsofhyperf/.github/.github/workflows/release.yaml@main \ No newline at end of file diff --git a/src/closure-job/LICENSE b/src/closure-job/LICENSE new file mode 100644 index 000000000..439eee00d --- /dev/null +++ b/src/closure-job/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) D.J.Hwang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/rate-limit/.github/FUNDING.yml b/src/rate-limit/.github/FUNDING.yml new file mode 100644 index 000000000..e52b3cbcd --- /dev/null +++ b/src/rate-limit/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: huangdijia +custom: https://hdj.me/sponsors/ \ No newline at end of file diff --git a/src/rate-limit/.github/workflows/close-pull-request.yml b/src/rate-limit/.github/workflows/close-pull-request.yml new file mode 100644 index 000000000..8605acc19 --- /dev/null +++ b/src/rate-limit/.github/workflows/close-pull-request.yml @@ -0,0 +1,9 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + uses: friendsofhyperf/.github/.github/workflows/close-pull-request.yml@main diff --git a/src/rate-limit/.github/workflows/release.yaml b/src/rate-limit/.github/workflows/release.yaml new file mode 100644 index 000000000..f924464bf --- /dev/null +++ b/src/rate-limit/.github/workflows/release.yaml @@ -0,0 +1,10 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + build: + uses: friendsofhyperf/.github/.github/workflows/release.yaml@main \ No newline at end of file From 9c0760234100307bdce986a7f5dedef1da1c9637 Mon Sep 17 00:00:00 2001 From: huangdijia <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 00:57:20 +0000 Subject: [PATCH 22/39] Update docs and translate --- docs/en/guide/start/components.md | 56 +++++++++++++++------------- docs/zh-hk/guide/start/components.md | 3 +- docs/zh-tw/guide/start/components.md | 3 +- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/docs/en/guide/start/components.md b/docs/en/guide/start/components.md index 7cb32fc02..35159cf70 100644 --- a/docs/en/guide/start/components.md +++ b/docs/en/guide/start/components.md @@ -1,31 +1,35 @@ # Components -## Supported Components List +## List of Supported Components | Repository | Stable Version | Total Downloads | Monthly Downloads | |--|--|--|--| -| [amqp-job](https://github.com/friendsofhyperf/amqp-job) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/amqp-job/v)](https://packagist.org/packages/friendsofhyperf/amqp-job) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/downloads)](https://packagist.org/packages/friendsofhyperf/amqp-job) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/amqp-job) | -| [cache](https://github.com/friendsofhyperf/cache) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/cache/v)](https://packagist.org/packages/friendsofhyperf/cache) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/cache/downloads)](https://packagist.org/packages/friendsofhyperf/cache) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/cache/d/monthly)](https://packagist.org/packages/friendsofhyperf/cache) | -| [command-signals](https://github.com/friendsofhyperf/command-signals) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-signals/v)](https://packagist.org/packages/friendsofhyperf/command-signals) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/downloads)](https://packagist.org/packages/friendsofhyperf/command-signals) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-signals) | -| [command-validation](https://github.com/friendsofhyperf/command-validation) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-validation/v)](https://packagist.org/packages/friendsofhyperf/command-validation) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-validation/downloads)](https://packagist.org/packages/friendsofhyperf/command-validation) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-validation/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-validation) | -| [compoships](https://github.com/friendsofhyperf/compoships) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/compoships/v)](https://packagist.org/packages/friendsofhyperf/compoships) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/compoships/downloads)](https://packagist.org/packages/friendsofhyperf/compoships) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/compoships/d/monthly)](https://packagist.org/packages/friendsofhyperf/compoships) | -| [confd](https://github.com/friendsofhyperf/confd) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/confd/v)](https://packagist.org/packages/friendsofhyperf/confd) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/confd/downloads)](https://packagist.org/packages/friendsofhyperf/confd) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/confd/d/monthly)](https://packagist.org/packages/friendsofhyperf/confd) | -| [config-consul](https://github.com/friendsofhyperf/config-consul) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/config-consul/v)](https://packagist.org/packages/friendsofhyperf/config-consul) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/config-consul/downloads)](https://packagist.org/packages/friendsofhyperf/config-consul) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/config-consul/d/monthly)](https://packagist.org/packages/friendsofhyperf/config-consul) | -| [console-spinner](https://github.com/friendsofhyperf/console-spinner) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/console-spinner/v)](https://packagist.org/packages/friendsofhyperf/console-spinner) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/console-spinner/downloads)](https://packagist.org/packages/friendsofhyperf/console-spinner) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/console-spinner/d/monthly)](https://packagist.org/packages/friendsofhyperf/console-spinner) | -| [elasticsearch](https://github.com/friendsofhyperf/elasticsearch) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/elasticsearch/v)](https://packagist.org/packages/friendsofhyperf/elasticsearch) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/elasticsearch/downloads)](https://packagist.org/packages/friendsofhyperf/elasticsearch) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/elasticsearch/d/monthly)](https://packagist.org/packages/friendsofhyperf/elasticsearch) | -| [encryption](https://github.com/friendsofhyperf/encryption) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/encryption/v)](https://packagist.org/packages/friendsofhyperf/encryption) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/encryption/downloads)](https://packagist.org/packages/friendsofhyperf/encryption) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/encryption/d/monthly)](https://packagist.org/packages/friendsofhyperf/encryption) | -| [exception-event](https://github.com/friendsofhyperf/exception-event) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/exception-event/v)](https://packagist.org/packages/friendsofhyperf/exception-event) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/exception-event/downloads)](https://packagist.org/packages/friendsofhyperf/exception-event) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/exception-event/d/monthly)](https://packagist.org/packages/friendsofhyperf/exception-event) | -| [facade](https://github.com/friendsofhyperf/facade) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/facade/v)](https://packagist.org/packages/friendsofhyperf/facade) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/facade/downloads)](https://packagist.org/packages/friendsofhyperf/facade) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/facade/d/monthly)](https://packagist.org/packages/friendsofhyperf/facade) | -| [fast-paginate](https://github.com/friendsofhyperf/fast-paginate) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/fast-paginate/v)](https://packagist.org/packages/friendsofhyperf/fast-paginate) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/fast-paginate/downloads)](https://packagist.org/packages/friendsofhyperf/fast-paginate) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/fast-paginate/d/monthly)](https://packagist.org/packages/friendsofhyperf/fast-paginate) | -| [grpc-validation](https://github.com/friendsofhyperf/grpc-validation) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/grpc-validation/v)](https://packagist.org/packages/friendsofhyperf/grpc-validation) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/grpc-validation/downloads)](https://packagist.org/packages/friendsofhyperf/grpc-validation) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/grpc-validation/d/monthly)](https://packagist.org/packages/friendsofhyperf/grpc-validation) | -| [helpers](https://github.com/friendsofhyperf/helpers) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/helpers/v)](https://packagist.org/packages/friendsofhyperf/helpers) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/helpers/downloads)](https://packagist.org/packages/friendsofhyperf/helpers) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/helpers/d/monthly)](https://packagist.org/packages/friendsofhyperf/helpers) | -| [http-client](https://github.com/friendsofhyperf/http-client) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/http-client/v)](https://packagist.org/packages/friendsofhyperf/http-client) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/http-client/downloads)](https://packagist.org/packages/friendsofhyperf/http-client) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/http-client/d/monthly)](https://packagist.org/packages/friendsofhyperf/http-client) | -| [ide-helper](https://github.com/friendsofhyperf/ide-helper) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/ide-helper/v)](https://packagist.org/packages/friendsofhyperf/ide-helper) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/ide-helper/downloads)](https://packagist.org/packages/friendsofhyperf/ide-helper) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/ide-helper/d/monthly)](https://packagist.org/packages/friendsofhyperf/ide-helper) | -| [ipc-broadcaster](https://github.com/friendsofhyperf/ipc-broadcaster) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/ipc-broadcaster/v)](https://packagist.org/packages/friendsofhyperf/ipc-broadcaster) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/ipc-broadcaster/downloads)](https://packagist.org/packages/friendsofhyperf/ipc-broadcaster) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/ipc-broadcaster/d/monthly)](https://packagist.org/packages/friendsofhyperf/ipc-broadcaster) | -| [lock](https://github.com/friendsofhyperf/lock) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/lock/v)](https://packagist.org/packages/friendsofhyperf/lock) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/lock/downloads)](https://packagist.org/packages/friendsofhyperf/lock) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/lock/d/monthly)](https://packagist.org/packages/friendsofhyperf/lock) | -| [macros](https://github.com/friendsofhyperf/macros) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/macros/v)](https://packagist.org/packages/friendsofhyperf/macros) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/macros/downloads)](https://packagist.org/packages/friendsofhyperf/macros) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/macros/d/monthly)](https://packagist.org/packages/friendsofhyperf/macros) | -| [mail](https://github.com/friendsofhyperf/mail) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/mail/v)](https://packagist.org/packages/friendsofhyperf/mail) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/mail/downloads)](https://packagist.org/packages/friendsofhyperf/mail) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/mail/d/monthly)](https://packagist.org/packages/friendsofhyperf/mail) | -| [model-factory](https://github.com/friendsofhyperf/model-factory) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-factory/v)](https://packagist.org/packages/friendsofhyperf/model-factory) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-factory/downloads)](https://packagist.org/packages/friendsofhyperf/model-factory) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-factory/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-factory) | -| [model-hashids](https://github.com/friendsofhyperf/model-hashids) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-hashids/v)](https://packagist.org/packages/friendsofhyperf/model-hashids) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-hashids/downloads)](https://packagist.org/packages/friendsofhyperf/model-hashids) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-hashids/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-hashids) | -| [model-morph-addon](https://github.com/friendsofhyperf/model-morph-addon) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-morph-addon/v)](https://packagist.org/packages/friendsofhyperf/model-morph-addon) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-morph-addon/downloads)](https://packagist.org/packages/friendsofhyperf/model-morph-addon) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-morph-addon/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-morph-addon) | -| [model-observer](https://github.com/friendsofhyperf/model-observer) | [![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-observer/v)](https://packagist.org/packages/friendsofhyperf/model-observer) | [![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-observer/downloads)](https://packagist.org/packages/friendsofhyperf/model-observer) | [![Monthly Downloads](https://poser.pugx.org/friendsofhyper \ No newline at end of file +|[amqp-job](https://github.com/friendsofhyperf/amqp-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/amqp-job/v)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/downloads)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/amqp-job)| +|[cache](https://github.com/friendsofhyperf/cache)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/cache/v)](https://packagist.org/packages/friendsofhyperf/cache)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/cache/downloads)](https://packagist.org/packages/friendsofhyperf/cache)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/cache/d/monthly)](https://packagist.org/packages/friendsofhyperf/cache)| +|[closure-job](https://github.com/friendsofhyperf/closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/closure-job/v)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/closure-job)| +|[co-phpunit](https://github.com/friendsofhyperf/co-phpunit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/co-phpunit/v)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/downloads)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/d/monthly)](https://packagist.org/packages/friendsofhyperf/co-phpunit)| +|[command-benchmark](https://github.com/friendsofhyperf/command-benchmark)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-benchmark/v)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/downloads)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-benchmark)| +|[command-signals](https://github.com/friendsofhyperf/command-signals)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-signals/v)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/downloads)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-signals)| +|[command-validation](https://github.com/friendsofhyperf/command-validation)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-validation/v)](https://packagist.org/packages/friendsofhyperf/command-validation)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-validation/downloads)](https://packagist.org/packages/friendsofhyperf/command-validation)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-validation/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-validation)| +|[compoships](https://github.com/friendsofhyperf/compoships)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/compoships/v)](https://packagist.org/packages/friendsofhyperf/compoships)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/compoships/downloads)](https://packagist.org/packages/friendsofhyperf/compoships)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/compoships/d/monthly)](https://packagist.org/packages/friendsofhyperf/compoships)| +|[confd](https://github.com/friendsofhyperf/confd)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/confd/v)](https://packagist.org/packages/friendsofhyperf/confd)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/confd/downloads)](https://packagist.org/packages/friendsofhyperf/confd)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/confd/d/monthly)](https://packagist.org/packages/friendsofhyperf/confd)| +|[config-consul](https://github.com/friendsofhyperf/config-consul)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/config-consul/v)](https://packagist.org/packages/friendsofhyperf/config-consul)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/config-consul/downloads)](https://packagist.org/packages/friendsofhyperf/config-consul)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/config-consul/d/monthly)](https://packagist.org/packages/friendsofhyperf/config-consul)| +|[console-spinner](https://github.com/friendsofhyperf/console-spinner)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/console-spinner/v)](https://packagist.org/packages/friendsofhyperf/console-spinner)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/console-spinner/downloads)](https://packagist.org/packages/friendsofhyperf/console-spinner)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/console-spinner/d/monthly)](https://packagist.org/packages/friendsofhyperf/console-spinner)| +|[elasticsearch](https://github.com/friendsofhyperf/elasticsearch)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/elasticsearch/v)](https://packagist.org/packages/friendsofhyperf/elasticsearch)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/elasticsearch/downloads)](https://packagist.org/packages/friendsofhyperf/elasticsearch)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/elasticsearch/d/monthly)](https://packagist.org/packages/friendsofhyperf/elasticsearch)| +|[encryption](https://github.com/friendsofhyperf/encryption)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/encryption/v)](https://packagist.org/packages/friendsofhyperf/encryption)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/encryption/downloads)](https://packagist.org/packages/friendsofhyperf/encryption)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/encryption/d/monthly)](https://packagist.org/packages/friendsofhyperf/encryption)| +|[exception-event](https://github.com/friendsofhyperf/exception-event)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/exception-event/v)](https://packagist.org/packages/friendsofhyperf/exception-event)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/exception-event/downloads)](https://packagist.org/packages/friendsofhyperf/exception-event)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/exception-event/d/monthly)](https://packagist.org/packages/friendsofhyperf/exception-event)| +|[facade](https://github.com/friendsofhyperf/facade)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/facade/v)](https://packagist.org/packages/friendsofhyperf/facade)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/facade/downloads)](https://packagist.org/packages/friendsofhyperf/facade)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/facade/d/monthly)](https://packagist.org/packages/friendsofhyperf/facade)| +|[fast-paginate](https://github.com/friendsofhyperf/fast-paginate)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/fast-paginate/v)](https://packagist.org/packages/friendsofhyperf/fast-paginate)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/fast-paginate/downloads)](https://packagist.org/packages/friendsofhyperf/fast-paginate)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/fast-paginate/d/monthly)](https://packagist.org/packages/friendsofhyperf/fast-paginate)| +|[grpc-validation](https://github.com/friendsofhyperf/grpc-validation)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/grpc-validation/v)](https://packagist.org/packages/friendsofhyperf/grpc-validation)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/grpc-validation/downloads)](https://packagist.org/packages/friendsofhyperf/grpc-validation)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/grpc-validation/d/monthly)](https://packagist.org/packages/friendsofhyperf/grpc-validation)| +|[helpers](https://github.com/friendsofhyperf/helpers)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/helpers/v)](https://packagist.org/packages/friendsofhyperf/helpers)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/helpers/downloads)](https://packagist.org/packages/friendsofhyperf/helpers)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/helpers/d/monthly)](https://packagist.org/packages/friendsofhyperf/helpers)| +|[http-client](https://github.com/friendsofhyperf/http-client)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/http-client/v)](https://packagist.org/packages/friendsofhyperf/http-client)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/http-client/downloads)](https://packagist.org/packages/friendsofhyperf/http-client)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/http-client/d/monthly)](https://packagist.org/packages/friendsofhyperf/http-client)| +|[ide-helper](https://github.com/friendsofhyperf/ide-helper)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/ide-helper/v)](https://packagist.org/packages/friendsofhyperf/ide-helper)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/ide-helper/downloads)](https://packagist.org/packages/friendsofhyperf/ide-helper)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/ide-helper/d/monthly)](https://packagist.org/packages/friendsofhyperf/ide-helper)| +|[ipc-broadcaster](https://github.com/friendsofhyperf/ipc-broadcaster)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/ipc-broadcaster/v)](https://packagist.org/packages/friendsofhyperf/ipc-broadcaster)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/ipc-broadcaster/downloads)](https://packagist.org/packages/friendsofhyperf/ipc-broadcaster)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/ipc-broadcaster/d/monthly)](https://packagist.org/packages/friendsofhyperf/ipc-broadcaster)| +|[lock](https://github.com/friendsofhyperf/lock)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/lock/v)](https://packagist.org/packages/friendsofhyperf/lock)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/lock/downloads)](https://packagist.org/packages/friendsofhyperf/lock)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/lock/d/monthly)](https://packagist.org/packages/friendsofhyperf/lock)| +|[macros](https://github.com/friendsofhyperf/macros)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/macros/v)](https://packagist.org/packages/friendsofhyperf/macros)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/macros/downloads)](https://packagist.org/packages/friendsofhyperf/macros)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/macros/d/monthly)](https://packagist.org/packages/friendsofhyperf/macros)| +|[mail](https://github.com/friendsofhyperf/mail)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/mail/v)](https://packagist.org/packages/friendsofhyperf/mail)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/mail/downloads)](https://packagist.org/packages/friendsofhyperf/mail)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/mail/d/monthly)](https://packagist.org/packages/friendsofhyperf/mail)| +|[model-factory](https://github.com/friendsofhyperf/model-factory)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-factory/v)](https://packagist.org/packages/friendsofhyperf/model-factory)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-factory/downloads)](https://packagist.org/packages/friendsofhyperf/model-factory)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-factory/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-factory)| +|[model-hashids](https://github.com/friendsofhyperf/model-hashids)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-hashids/v)](https://packagist.org/packages/friendsofhyperf/model-hashids)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-hashids/downloads)](https://packagist.org/packages/friendsofhyperf/model-hashids)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-hashids/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-hashids)| +|[model-morph-addon](https://github.com/friendsofhyperf/model-morph-addon)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-morph-addon/v)](https://packagist.org/packages/friendsofhyperf/model-morph-addon)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-morph-addon/downloads)](https://packagist.org/packages/friendsofhyperf/model-morph-addon)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-morph-addon/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-morph-addon)| +|[model-observer](https://github.com/friendsofhyperf/model-observer)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-observer/v)](https://packagist.org/packages/friendsofhyperf/model-observer)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/model-observer/downloads)](https://packagist.org/packages/friendsofhyperf/model-observer)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/model-observer/d/monthly)](https://packagist.org/packages/friendsofhyperf/model-observer)| +|[model-scope](https://github.com/friendsofhyperf/model-scope)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/model-scope/v)](https://packagist.org/packages/friendsofhyperf/model-scope)|[![Total Downloads \ No newline at end of file diff --git a/docs/zh-hk/guide/start/components.md b/docs/zh-hk/guide/start/components.md index 66cff44bc..5187471c3 100644 --- a/docs/zh-hk/guide/start/components.md +++ b/docs/zh-hk/guide/start/components.md @@ -5,8 +5,8 @@ |Repository|Stable Version|Total Downloads|Monthly Downloads| |--|--|--|--| |[amqp-job](https://github.com/friendsofhyperf/amqp-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/amqp-job/v)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/downloads)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/amqp-job)| -|[async-queue-closure-job](https://github.com/friendsofhyperf/async-queue-closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/async-queue-closure-job/v)](https://packagist.org/packages/friendsofhyperf/async-queue-closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/async-queue-closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/async-queue-closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/async-queue-closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/async-queue-closure-job)| |[cache](https://github.com/friendsofhyperf/cache)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/cache/v)](https://packagist.org/packages/friendsofhyperf/cache)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/cache/downloads)](https://packagist.org/packages/friendsofhyperf/cache)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/cache/d/monthly)](https://packagist.org/packages/friendsofhyperf/cache)| +|[closure-job](https://github.com/friendsofhyperf/closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/closure-job/v)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/closure-job)| |[co-phpunit](https://github.com/friendsofhyperf/co-phpunit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/co-phpunit/v)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/downloads)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/d/monthly)](https://packagist.org/packages/friendsofhyperf/co-phpunit)| |[command-benchmark](https://github.com/friendsofhyperf/command-benchmark)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-benchmark/v)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/downloads)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-benchmark)| |[command-signals](https://github.com/friendsofhyperf/command-signals)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-signals/v)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/downloads)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-signals)| @@ -42,6 +42,7 @@ |[openai-client](https://github.com/friendsofhyperf/openai-client)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/openai-client/v)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/downloads)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/d/monthly)](https://packagist.org/packages/friendsofhyperf/openai-client)| |[pretty-console](https://github.com/friendsofhyperf/pretty-console)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/pretty-console/v)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/downloads)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/d/monthly)](https://packagist.org/packages/friendsofhyperf/pretty-console)| |[purifier](https://github.com/friendsofhyperf/purifier)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/purifier/v)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/purifier/downloads)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/purifier/d/monthly)](https://packagist.org/packages/friendsofhyperf/purifier)| +|[rate-limit](https://github.com/friendsofhyperf/rate-limit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/rate-limit/v)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/downloads)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/d/monthly)](https://packagist.org/packages/friendsofhyperf/rate-limit)| |[recaptcha](https://github.com/friendsofhyperf/recaptcha)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/recaptcha/v)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/downloads)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/d/monthly)](https://packagist.org/packages/friendsofhyperf/recaptcha)| |[redis-subscriber](https://github.com/friendsofhyperf/redis-subscriber)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/redis-subscriber/v)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/downloads)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/d/monthly)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)| |[sentry](https://github.com/friendsofhyperf/sentry)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/sentry/v)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/sentry/downloads)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/sentry/d/monthly)](https://packagist.org/packages/friendsofhyperf/sentry)| diff --git a/docs/zh-tw/guide/start/components.md b/docs/zh-tw/guide/start/components.md index 1ca3569b4..ac9d1ac3b 100644 --- a/docs/zh-tw/guide/start/components.md +++ b/docs/zh-tw/guide/start/components.md @@ -5,8 +5,8 @@ |Repository|Stable Version|Total Downloads|Monthly Downloads| |--|--|--|--| |[amqp-job](https://github.com/friendsofhyperf/amqp-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/amqp-job/v)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/downloads)](https://packagist.org/packages/friendsofhyperf/amqp-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/amqp-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/amqp-job)| -|[async-queue-closure-job](https://github.com/friendsofhyperf/async-queue-closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/async-queue-closure-job/v)](https://packagist.org/packages/friendsofhyperf/async-queue-closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/async-queue-closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/async-queue-closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/async-queue-closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/async-queue-closure-job)| |[cache](https://github.com/friendsofhyperf/cache)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/cache/v)](https://packagist.org/packages/friendsofhyperf/cache)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/cache/downloads)](https://packagist.org/packages/friendsofhyperf/cache)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/cache/d/monthly)](https://packagist.org/packages/friendsofhyperf/cache)| +|[closure-job](https://github.com/friendsofhyperf/closure-job)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/closure-job/v)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/downloads)](https://packagist.org/packages/friendsofhyperf/closure-job)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/closure-job/d/monthly)](https://packagist.org/packages/friendsofhyperf/closure-job)| |[co-phpunit](https://github.com/friendsofhyperf/co-phpunit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/co-phpunit/v)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/downloads)](https://packagist.org/packages/friendsofhyperf/co-phpunit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/co-phpunit/d/monthly)](https://packagist.org/packages/friendsofhyperf/co-phpunit)| |[command-benchmark](https://github.com/friendsofhyperf/command-benchmark)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-benchmark/v)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/downloads)](https://packagist.org/packages/friendsofhyperf/command-benchmark)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-benchmark/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-benchmark)| |[command-signals](https://github.com/friendsofhyperf/command-signals)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/command-signals/v)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/downloads)](https://packagist.org/packages/friendsofhyperf/command-signals)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/command-signals/d/monthly)](https://packagist.org/packages/friendsofhyperf/command-signals)| @@ -42,6 +42,7 @@ |[openai-client](https://github.com/friendsofhyperf/openai-client)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/openai-client/v)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/downloads)](https://packagist.org/packages/friendsofhyperf/openai-client)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/openai-client/d/monthly)](https://packagist.org/packages/friendsofhyperf/openai-client)| |[pretty-console](https://github.com/friendsofhyperf/pretty-console)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/pretty-console/v)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/downloads)](https://packagist.org/packages/friendsofhyperf/pretty-console)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/pretty-console/d/monthly)](https://packagist.org/packages/friendsofhyperf/pretty-console)| |[purifier](https://github.com/friendsofhyperf/purifier)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/purifier/v)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/purifier/downloads)](https://packagist.org/packages/friendsofhyperf/purifier)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/purifier/d/monthly)](https://packagist.org/packages/friendsofhyperf/purifier)| +|[rate-limit](https://github.com/friendsofhyperf/rate-limit)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/rate-limit/v)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/downloads)](https://packagist.org/packages/friendsofhyperf/rate-limit)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/d/monthly)](https://packagist.org/packages/friendsofhyperf/rate-limit)| |[recaptcha](https://github.com/friendsofhyperf/recaptcha)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/recaptcha/v)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/downloads)](https://packagist.org/packages/friendsofhyperf/recaptcha)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/recaptcha/d/monthly)](https://packagist.org/packages/friendsofhyperf/recaptcha)| |[redis-subscriber](https://github.com/friendsofhyperf/redis-subscriber)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/redis-subscriber/v)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/downloads)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/redis-subscriber/d/monthly)](https://packagist.org/packages/friendsofhyperf/redis-subscriber)| |[sentry](https://github.com/friendsofhyperf/sentry)|[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/sentry/v)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Total Downloads](https://poser.pugx.org/friendsofhyperf/sentry/downloads)](https://packagist.org/packages/friendsofhyperf/sentry)|[![Monthly Downloads](https://poser.pugx.org/friendsofhyperf/sentry/d/monthly)](https://packagist.org/packages/friendsofhyperf/sentry)| From 52ae78740b5ec108363b9c318bbf399549fb91eb Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:08:29 +0800 Subject: [PATCH 23/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E4=BF=AE=E6=AD=A3=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=8F=82=E6=95=B0=E7=9A=84=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E4=BB=8E=E6=AD=A3=E7=A1=AE=E7=9A=84?= =?UTF-8?q?=E9=94=AE=E4=B8=AD=E6=8F=90=E5=8F=96=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 87a437d57..8eb56ba7d 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -76,7 +76,7 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding $placeholder = $matches[1]; // Try to get from method arguments - $arguments = $proceedingJoinPoint->arguments; + $arguments = $proceedingJoinPoint->arguments['keys']; foreach ($arguments['keys'] ?? [] as $argKey => $argValue) { if ($argKey === $placeholder) { return (string) $argValue; From 5ea3dc72e02c7224fa8f9bc809aa1520320f2aef Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:10:26 +0800 Subject: [PATCH 24/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20RE?= =?UTF-8?q?ADME=5FCN.md=20=E4=B8=AD=E6=9B=B4=E6=96=B0=E9=99=90=E6=B5=81?= =?UTF-8?q?=E7=AE=97=E6=B3=95=E7=9A=84=E4=BD=BF=E7=94=A8=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E4=B8=BA=E4=BD=BF=E7=94=A8=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=AE=97=E6=B3=95=E7=B1=BB=E5=9E=8B=EF=BC=9B?= =?UTF-8?q?=E5=9C=A8=20RateLimitAspect.php=20=E4=B8=AD=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E5=BC=95=E7=94=A8=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E8=A7=A3=E6=9E=90=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/README_CN.md | 29 +++++++++++++------ src/rate-limit/src/Aspect/RateLimitAspect.php | 8 +++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/rate-limit/README_CN.md b/src/rate-limit/README_CN.md index 643dc5cee..bb279bb08 100644 --- a/src/rate-limit/README_CN.md +++ b/src/rate-limit/README_CN.md @@ -34,17 +34,18 @@ php bin/hyperf.php vendor:publish friendsofhyperf/rate-limit ### 使用注解 ```php +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\Annotation\RateLimit; class UserController { - #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: "sliding_window")] + #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: Algorithm::SLIDING_WINDOW)] public function index() { return ['message' => 'Hello World']; } - #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: "fixed_window")] + #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: Algorithm::FIXED_WINDOW)] public function login() { // 登录逻辑 @@ -59,6 +60,7 @@ class UserController ```php namespace App\Middleware; +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\Middleware\RateLimitMiddleware; use Psr\Http\Message\ServerRequestInterface; @@ -66,7 +68,7 @@ class ApiRateLimitMiddleware extends RateLimitMiddleware { protected int $maxAttempts = 60; protected int $decay = 60; - protected string $algorithm = 'sliding_window'; + protected Algorithm $algorithm = Algorithm::SLIDING_WINDOW; protected function resolveKey(ServerRequestInterface $request): string { @@ -80,6 +82,7 @@ class ApiRateLimitMiddleware extends RateLimitMiddleware ### 在代码中直接使用 ```php +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\RateLimiterFactory; class YourService @@ -90,8 +93,8 @@ class YourService public function someMethod() { - $limiter = $this->factory->make('sliding_window'); - + $limiter = $this->factory->make(Algorithm::SLIDING_WINDOW); + $key = 'operation:user:123'; $maxAttempts = 10; $decay = 60; @@ -112,7 +115,9 @@ class YourService 简单的计数器,在固定时间间隔重置。速度快但可能在窗口边界出现突发流量。 ```php -#[RateLimit(algorithm: "fixed_window", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::FIXED_WINDOW, maxAttempts: 100, decay: 60)] ``` ### 滑动窗口(Sliding Window) @@ -120,7 +125,9 @@ class YourService 比固定窗口更精确,使用有序集合跟踪带时间戳的请求。 ```php -#[RateLimit(algorithm: "sliding_window", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::SLIDING_WINDOW, maxAttempts: 100, decay: 60)] ``` ### 令牌桶(Token Bucket) @@ -128,7 +135,9 @@ class YourService 允许突发流量达到桶容量,令牌以恒定速率添加。 ```php -#[RateLimit(algorithm: "token_bucket", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::TOKEN_BUCKET, maxAttempts: 100, decay: 60)] ``` ### 漏桶(Leaky Bucket) @@ -136,7 +145,9 @@ class YourService 平滑突发流量,无论到达模式如何,都以恒定速率处理请求。 ```php -#[RateLimit(algorithm: "leaky_bucket", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::LEAKY_BUCKET, maxAttempts: 100, decay: 60)] ``` ## 配置 diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 8eb56ba7d..d01396966 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -17,6 +17,8 @@ use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; +use function Hyperf\Collection\data_get; + class RateLimitAspect extends AbstractAspect { public array $annotations = [ @@ -78,6 +80,12 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding // Try to get from method arguments $arguments = $proceedingJoinPoint->arguments['keys']; foreach ($arguments['keys'] ?? [] as $argKey => $argValue) { + if ( + (is_array($argValue) || is_object($argValue)) + && str_contains($placeholder, '.') + ) { + return (string) data_get($argValue, str_replace('.', '', $placeholder)); + } if ($argKey === $placeholder) { return (string) $argValue; } From 9e3a2eeaad69be3188325f17f15d8f0ed9ddbfce Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:12:55 +0800 Subject: [PATCH 25/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20RE?= =?UTF-8?q?ADME.md=20=E4=B8=AD=E6=9B=B4=E6=96=B0=E9=99=90=E6=B5=81?= =?UTF-8?q?=E7=AE=97=E6=B3=95=E7=9A=84=E4=BD=BF=E7=94=A8=E7=A4=BA=E4=BE=8B?= =?UTF-8?q?=EF=BC=8C=E6=94=B9=E4=B8=BA=E4=BD=BF=E7=94=A8=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=AE=97=E6=B3=95=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/rate-limit/README.md b/src/rate-limit/README.md index 6ff6dc25d..697d1c36a 100644 --- a/src/rate-limit/README.md +++ b/src/rate-limit/README.md @@ -34,17 +34,18 @@ This will create a `config/autoload/rate_limit.php` configuration file. ### Using Annotations ```php +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\Annotation\RateLimit; class UserController { - #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: "sliding_window")] + #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: Algorithm::SLIDING_WINDOW)] public function index() { return ['message' => 'Hello World']; } - #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: "fixed_window")] + #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: Algorithm::FIXED_WINDOW)] public function login() { // Login logic @@ -59,6 +60,7 @@ Create a custom middleware that extends `RateLimitMiddleware`: ```php namespace App\Middleware; +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\Middleware\RateLimitMiddleware; use Psr\Http\Message\ServerRequestInterface; @@ -66,7 +68,7 @@ class ApiRateLimitMiddleware extends RateLimitMiddleware { protected int $maxAttempts = 60; protected int $decay = 60; - protected string $algorithm = 'sliding_window'; + protected Algorithm $algorithm = Algorithm::SLIDING_WINDOW; protected function resolveKey(ServerRequestInterface $request): string { @@ -80,6 +82,7 @@ Then register it in your middleware configuration. ### Using Directly in Code ```php +use FriendsOfHyperf\RateLimit\Algorithm; use FriendsOfHyperf\RateLimit\RateLimiterFactory; class YourService @@ -90,8 +93,8 @@ class YourService public function someMethod() { - $limiter = $this->factory->make('sliding_window'); - + $limiter = $this->factory->make(Algorithm::SLIDING_WINDOW); + $key = 'operation:user:123'; $maxAttempts = 10; $decay = 60; @@ -112,7 +115,9 @@ class YourService Simple counter that resets at fixed intervals. Fast but can allow bursts at window boundaries. ```php -#[RateLimit(algorithm: "fixed_window", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::FIXED_WINDOW, maxAttempts: 100, decay: 60)] ``` ### Sliding Window @@ -120,7 +125,9 @@ Simple counter that resets at fixed intervals. Fast but can allow bursts at wind More accurate than fixed window, uses sorted sets to track requests with timestamps. ```php -#[RateLimit(algorithm: "sliding_window", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::SLIDING_WINDOW, maxAttempts: 100, decay: 60)] ``` ### Token Bucket @@ -128,7 +135,9 @@ More accurate than fixed window, uses sorted sets to track requests with timesta Allows bursts up to bucket capacity, tokens are added at a constant rate. ```php -#[RateLimit(algorithm: "token_bucket", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::TOKEN_BUCKET, maxAttempts: 100, decay: 60)] ``` ### Leaky Bucket @@ -136,7 +145,9 @@ Allows bursts up to bucket capacity, tokens are added at a constant rate. Smooths out bursts, processes requests at a constant rate regardless of arrival pattern. ```php -#[RateLimit(algorithm: "leaky_bucket", maxAttempts: 100, decay: 60)] +use FriendsOfHyperf\RateLimit\Algorithm; + +#[RateLimit(algorithm: Algorithm::LEAKY_BUCKET, maxAttempts: 100, decay: 60)] ``` ## Configuration From 748f7c406402951eaa156168fb142ec789eb1456 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:14:03 +0800 Subject: [PATCH 26/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E4=BF=AE=E6=AD=A3=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E9=94=AE=E7=9A=84=E8=BF=94=E5=9B=9E=E6=96=B9=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E6=96=B9=E6=B3=95=E7=AD=BE=E5=90=8D=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E9=BB=98=E8=AE=A4=E9=94=AE=E8=BF=94=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index d01396966..1762ea746 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -65,7 +65,7 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding // Use method signature as default key $className = $proceedingJoinPoint->className; $methodName = $proceedingJoinPoint->methodName; - $key = "{$className}:{$methodName}"; + return "{$className}:{$methodName}"; } if (is_callable($key)) { From c440eda271a303e13eb75f732bb29b3b8a7194ce Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:16:33 +0800 Subject: [PATCH 27/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E4=BF=AE=E6=AD=A3=E4=BB=8E=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=8F=82=E6=95=B0=E8=8E=B7=E5=8F=96=E9=94=AE=E7=9A=84?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E5=8F=82=E6=95=B0=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 1762ea746..dac160033 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -79,12 +79,12 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding // Try to get from method arguments $arguments = $proceedingJoinPoint->arguments['keys']; - foreach ($arguments['keys'] ?? [] as $argKey => $argValue) { + foreach ($arguments ?? [] as $argKey => $argValue) { if ( (is_array($argValue) || is_object($argValue)) && str_contains($placeholder, '.') ) { - return (string) data_get($argValue, str_replace('.', '', $placeholder)); + return (string) data_get($arguments, $placeholder); } if ($argKey === $placeholder) { return (string) $argValue; From d7dd9be4902860f9f688a0ef39cda56cc1110fae Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:19:00 +0800 Subject: [PATCH 28/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=AF=B9=E6=95=B0?= =?UTF-8?q?=E7=BB=84=E9=94=AE=E7=9A=84=E6=94=AF=E6=8C=81=EF=BC=8C=E5=B0=86?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E9=94=AE=E5=90=88=E5=B9=B6=E4=B8=BA=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index dac160033..9d380f7b8 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -72,6 +72,10 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding return $key($proceedingJoinPoint); } + if (is_array($key)) { + $key = implode(':', $key); + } + // Support placeholders like {user_id}, {ip}, etc. if (str_contains($key, '{')) { $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { From 29d561fac207b0744188a2795b95ef6c28c733e4 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:19:59 +0800 Subject: [PATCH 29/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E4=BF=AE=E6=AD=A3=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=8F=82=E6=95=B0=E7=9A=84=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=AD=A3=E7=A1=AE=E5=A4=84=E7=90=86?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=95=B0=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index 9d380f7b8..b41184ebd 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -81,9 +81,9 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { $placeholder = $matches[1]; - // Try to get from method arguments - $arguments = $proceedingJoinPoint->arguments['keys']; - foreach ($arguments ?? [] as $argKey => $argValue) { + /** @var array $arguments */ + $arguments = $proceedingJoinPoint->arguments['keys'] ?? []; + foreach ($arguments as $argKey => $argValue) { if ( (is_array($argValue) || is_object($argValue)) && str_contains($placeholder, '.') From 52b22be6301eafe454d2eeace9ebdc065633c8b2 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:23:06 +0800 Subject: [PATCH 30/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Ra?= =?UTF-8?q?teLimitAspect=20=E4=B8=AD=E4=BF=AE=E6=AD=A3=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E9=94=AE=E7=9A=84=E5=A4=84=E7=90=86=E6=96=B9=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E4=BD=BF=E7=94=A8=20array=5Fvalues=20?= =?UTF-8?q?=E4=BB=A5=E6=AD=A3=E7=A1=AE=E5=90=88=E5=B9=B6=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Aspect/RateLimitAspect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit/src/Aspect/RateLimitAspect.php b/src/rate-limit/src/Aspect/RateLimitAspect.php index b41184ebd..f4b7babee 100644 --- a/src/rate-limit/src/Aspect/RateLimitAspect.php +++ b/src/rate-limit/src/Aspect/RateLimitAspect.php @@ -73,7 +73,7 @@ protected function resolveKey(string|array $key, ProceedingJoinPoint $proceeding } if (is_array($key)) { - $key = implode(':', $key); + $key = implode(':', array_values($key)); } // Support placeholders like {user_id}, {ip}, etc. From 020c06a1a46351a4916aafbbf55324a37b37c888 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:24:27 +0800 Subject: [PATCH 31/39] =?UTF-8?q?=E5=88=A0=E9=99=A4=EF=BC=9A=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E9=99=90=E6=B5=81=E7=BB=84=E4=BB=B6=E7=9A=84=E4=B8=AD?= =?UTF-8?q?=E6=96=87=20README=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/README.md | 203 ------------------------------------ src/rate-limit/README_CN.md | 203 ------------------------------------ 2 files changed, 406 deletions(-) delete mode 100644 src/rate-limit/README.md delete mode 100644 src/rate-limit/README_CN.md diff --git a/src/rate-limit/README.md b/src/rate-limit/README.md deleted file mode 100644 index 697d1c36a..000000000 --- a/src/rate-limit/README.md +++ /dev/null @@ -1,203 +0,0 @@ -# Rate Limit - -[![Latest Version on Packagist](https://img.shields.io/packagist/v/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) -[![Total Downloads](https://img.shields.io/packagist/dt/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) - -Rate limiting component for Hyperf with support for multiple algorithms. - -## Features - -- **Multiple Algorithms**: Fixed Window, Sliding Window, Token Bucket, Leaky Bucket -- **Annotation Support**: Easy declarative rate limiting with `#[RateLimit]` annotation -- **AOP Integration**: Automatic rate limiting via aspect-oriented programming -- **Middleware**: HTTP middleware for request rate limiting -- **Redis + Lua**: Atomic operations using Redis with Lua scripts -- **Dynamic Configuration**: Support for config center integration -- **Flexible Key Resolution**: Support for placeholders like `{ip}`, `{user_id}`, etc. - -## Installation - -```bash -composer require friendsofhyperf/rate-limit -``` - -## Publish Configuration - -```bash -php bin/hyperf.php vendor:publish friendsofhyperf/rate-limit -``` - -This will create a `config/autoload/rate_limit.php` configuration file. - -## Usage - -### Using Annotations - -```php -use FriendsOfHyperf\RateLimit\Algorithm; -use FriendsOfHyperf\RateLimit\Annotation\RateLimit; - -class UserController -{ - #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: Algorithm::SLIDING_WINDOW)] - public function index() - { - return ['message' => 'Hello World']; - } - - #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: Algorithm::FIXED_WINDOW)] - public function login() - { - // Login logic - } -} -``` - -### Using Middleware - -Create a custom middleware that extends `RateLimitMiddleware`: - -```php -namespace App\Middleware; - -use FriendsOfHyperf\RateLimit\Algorithm; -use FriendsOfHyperf\RateLimit\Middleware\RateLimitMiddleware; -use Psr\Http\Message\ServerRequestInterface; - -class ApiRateLimitMiddleware extends RateLimitMiddleware -{ - protected int $maxAttempts = 60; - protected int $decay = 60; - protected Algorithm $algorithm = Algorithm::SLIDING_WINDOW; - - protected function resolveKey(ServerRequestInterface $request): string - { - return 'api:' . $this->getClientIp(); - } -} -``` - -Then register it in your middleware configuration. - -### Using Directly in Code - -```php -use FriendsOfHyperf\RateLimit\Algorithm; -use FriendsOfHyperf\RateLimit\RateLimiterFactory; - -class YourService -{ - public function __construct(private RateLimiterFactory $factory) - { - } - - public function someMethod() - { - $limiter = $this->factory->make(Algorithm::SLIDING_WINDOW); - - $key = 'operation:user:123'; - $maxAttempts = 10; - $decay = 60; - - if ($limiter->tooManyAttempts($key, $maxAttempts, $decay)) { - throw new \Exception('Rate limit exceeded'); - } - - // Your logic here - } -} -``` - -## Algorithms - -### Fixed Window - -Simple counter that resets at fixed intervals. Fast but can allow bursts at window boundaries. - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::FIXED_WINDOW, maxAttempts: 100, decay: 60)] -``` - -### Sliding Window - -More accurate than fixed window, uses sorted sets to track requests with timestamps. - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::SLIDING_WINDOW, maxAttempts: 100, decay: 60)] -``` - -### Token Bucket - -Allows bursts up to bucket capacity, tokens are added at a constant rate. - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::TOKEN_BUCKET, maxAttempts: 100, decay: 60)] -``` - -### Leaky Bucket - -Smooths out bursts, processes requests at a constant rate regardless of arrival pattern. - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::LEAKY_BUCKET, maxAttempts: 100, decay: 60)] -``` - -## Configuration - -The configuration file supports: - -```php -return [ - 'default' => 'fixed_window', - 'connection' => 'default', - 'prefix' => 'rate_limit', - - 'defaults' => [ - 'max_attempts' => 60, - 'decay' => 60, - ], - - 'limiters' => [ - 'api' => [ - 'max_attempts' => 60, - 'decay' => 60, - 'algorithm' => 'sliding_window', - ], - 'login' => [ - 'max_attempts' => 5, - 'decay' => 60, - 'algorithm' => 'fixed_window', - ], - ], -]; -``` - -## Key Placeholders - -The annotation supports dynamic placeholders in the key: - -- `{ip}` - Client IP address -- `{user_id}` - User ID from request attributes -- Any method argument name - -Example: - -```php -#[RateLimit(key: "api:{ip}:user:{user_id}", maxAttempts: 60, decay: 60)] -public function profile($userId) -{ - // ... -} -``` - -## License - -MIT diff --git a/src/rate-limit/README_CN.md b/src/rate-limit/README_CN.md deleted file mode 100644 index bb279bb08..000000000 --- a/src/rate-limit/README_CN.md +++ /dev/null @@ -1,203 +0,0 @@ -# 限流组件 - -[![Latest Version on Packagist](https://img.shields.io/packagist/v/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) -[![Total Downloads](https://img.shields.io/packagist/dt/friendsofhyperf/rate-limit.svg?style=flat-square)](https://packagist.org/packages/friendsofhyperf/rate-limit) - -为 Hyperf 提供的限流组件,支持多种限流算法。 - -## 特性 - -- **多种算法**:固定窗口、滑动窗口、令牌桶、漏桶 -- **注解支持**:使用 `#[RateLimit]` 注解轻松实现声明式限流 -- **AOP 集成**:通过面向切面编程自动限流 -- **中间件**:提供 HTTP 请求限流中间件 -- **Redis + Lua**:使用 Redis 和 Lua 脚本实现原子化操作 -- **动态配置**:支持配置中心集成 -- **灵活的键解析**:支持 `{ip}`、`{user_id}` 等占位符 - -## 安装 - -```bash -composer require friendsofhyperf/rate-limit -``` - -## 发布配置 - -```bash -php bin/hyperf.php vendor:publish friendsofhyperf/rate-limit -``` - -这将创建 `config/autoload/rate_limit.php` 配置文件。 - -## 使用 - -### 使用注解 - -```php -use FriendsOfHyperf\RateLimit\Algorithm; -use FriendsOfHyperf\RateLimit\Annotation\RateLimit; - -class UserController -{ - #[RateLimit(key: "api:{ip}", maxAttempts: 60, decay: 60, algorithm: Algorithm::SLIDING_WINDOW)] - public function index() - { - return ['message' => 'Hello World']; - } - - #[RateLimit(key: "login:{ip}", maxAttempts: 5, decay: 60, algorithm: Algorithm::FIXED_WINDOW)] - public function login() - { - // 登录逻辑 - } -} -``` - -### 使用中间件 - -创建一个继承 `RateLimitMiddleware` 的自定义中间件: - -```php -namespace App\Middleware; - -use FriendsOfHyperf\RateLimit\Algorithm; -use FriendsOfHyperf\RateLimit\Middleware\RateLimitMiddleware; -use Psr\Http\Message\ServerRequestInterface; - -class ApiRateLimitMiddleware extends RateLimitMiddleware -{ - protected int $maxAttempts = 60; - protected int $decay = 60; - protected Algorithm $algorithm = Algorithm::SLIDING_WINDOW; - - protected function resolveKey(ServerRequestInterface $request): string - { - return 'api:' . $this->getClientIp(); - } -} -``` - -然后在中间件配置中注册它。 - -### 在代码中直接使用 - -```php -use FriendsOfHyperf\RateLimit\Algorithm; -use FriendsOfHyperf\RateLimit\RateLimiterFactory; - -class YourService -{ - public function __construct(private RateLimiterFactory $factory) - { - } - - public function someMethod() - { - $limiter = $this->factory->make(Algorithm::SLIDING_WINDOW); - - $key = 'operation:user:123'; - $maxAttempts = 10; - $decay = 60; - - if ($limiter->tooManyAttempts($key, $maxAttempts, $decay)) { - throw new \Exception('超出限流次数'); - } - - // 你的业务逻辑 - } -} -``` - -## 算法说明 - -### 固定窗口(Fixed Window) - -简单的计数器,在固定时间间隔重置。速度快但可能在窗口边界出现突发流量。 - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::FIXED_WINDOW, maxAttempts: 100, decay: 60)] -``` - -### 滑动窗口(Sliding Window) - -比固定窗口更精确,使用有序集合跟踪带时间戳的请求。 - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::SLIDING_WINDOW, maxAttempts: 100, decay: 60)] -``` - -### 令牌桶(Token Bucket) - -允许突发流量达到桶容量,令牌以恒定速率添加。 - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::TOKEN_BUCKET, maxAttempts: 100, decay: 60)] -``` - -### 漏桶(Leaky Bucket) - -平滑突发流量,无论到达模式如何,都以恒定速率处理请求。 - -```php -use FriendsOfHyperf\RateLimit\Algorithm; - -#[RateLimit(algorithm: Algorithm::LEAKY_BUCKET, maxAttempts: 100, decay: 60)] -``` - -## 配置 - -配置文件支持以下选项: - -```php -return [ - 'default' => 'fixed_window', - 'connection' => 'default', - 'prefix' => 'rate_limit', - - 'defaults' => [ - 'max_attempts' => 60, - 'decay' => 60, - ], - - 'limiters' => [ - 'api' => [ - 'max_attempts' => 60, - 'decay' => 60, - 'algorithm' => 'sliding_window', - ], - 'login' => [ - 'max_attempts' => 5, - 'decay' => 60, - 'algorithm' => 'fixed_window', - ], - ], -]; -``` - -## 键占位符 - -注解支持在键中使用动态占位符: - -- `{ip}` - 客户端 IP 地址 -- `{user_id}` - 从请求属性获取的用户 ID -- 任何方法参数名称 - -示例: - -```php -#[RateLimit(key: "api:{ip}:user:{user_id}", maxAttempts: 60, decay: 60)] -public function profile($userId) -{ - // ... -} -``` - -## 许可证 - -MIT From 6f676043ca94a927bcebb186c34f6609eeb193bc Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:25:51 +0800 Subject: [PATCH 32/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20co?= =?UTF-8?q?mposer.json=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=AF=B9=20hyperf/col?= =?UTF-8?q?lection=20=E7=9A=84=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rate-limit/composer.json b/src/rate-limit/composer.json index 4a6ab9a90..804d6f083 100644 --- a/src/rate-limit/composer.json +++ b/src/rate-limit/composer.json @@ -23,6 +23,7 @@ }, "require": { "php": ">=8.1", + "hyperf/collection": "~3.1.0", "hyperf/config": "~3.1.0", "hyperf/context": "~3.1.0", "hyperf/di": "~3.1.0", From a42b37c244bd7728e36f411d46e8ecdc7339da9f Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:27:14 +0800 Subject: [PATCH 33/39] Create README.md Co-Authored-By: Deeka Wong <8337659+huangdijia@users.noreply.github.com> --- src/rate-limit/README.md | 410 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 src/rate-limit/README.md diff --git a/src/rate-limit/README.md b/src/rate-limit/README.md new file mode 100644 index 000000000..18692b65f --- /dev/null +++ b/src/rate-limit/README.md @@ -0,0 +1,410 @@ +# Rate Limit + +[![Latest Stable Version](https://poser.pugx.org/friendsofhyperf/rate-limit/v)](https://packagist.org/packages/friendsofhyperf/rate-limit) +[![Total Downloads](https://poser.pugx.org/friendsofhyperf/rate-limit/downloads)](https://packagist.org/packages/friendsofhyperf/rate-limit) +[![License](https://poser.pugx.org/friendsofhyperf/rate-limit/license)](https://packagist.org/packages/friendsofhyperf/rate-limit) + +Rate limiting component for Hyperf with support for multiple algorithms (Fixed Window, Sliding Window, Token Bucket, Leaky Bucket). + +## Installation + +```bash +composer require friendsofhyperf/rate-limit +``` + +## Requirements + +- PHP >= 8.1 +- Hyperf ~3.1.0 +- Redis + +## Features + +- **Multiple Rate Limiting Algorithms** + - Fixed Window + - Sliding Window + - Token Bucket + - Leaky Bucket +- **Flexible Usage** + - Annotation-based rate limiting via Aspect + - Custom middleware support +- **Flexible Key Generation** + - Default method/class-based keys + - Custom key with placeholders support + - Array keys support + - Callable keys support +- **Customizable Responses** + - Custom response message + - Custom HTTP response code +- **Multi Redis Pool Support** + +## Usage + +### Method 1: Using Annotagion + +The easiest way to add rate limiting is using the `#[RateLimit]` attribute: + +```php +getClientIp(); + } +} +``` + +Then register the middleware in your config: + +```php +// config/autoload/middlewares.php +return [ + 'http' => [ + App\Middleware\ApiRateLimitMiddleware::class, + ], +]; +``` + +### Rate Limiting Algorithms + +#### Fixed Window (默认) + +Simplest algorithm, counts requests in fixed time windows. + +```php +#[RateLimit(algorithm: Algorithm::FIXED_WINDOW)] +``` + +**Pros**: Simple, memory efficient +**Cons**: Can allow burst requests at window boundaries + +#### Sliding Window + +More accurate than fixed window, spreads requests evenly. + +```php +#[RateLimit(algorithm: Algorithm::SLIDING_WINDOW)] +``` + +**Pros**: Smooths out bursts, more accurate +**Cons**: Slightly more complex + +#### Token Bucket + +Allows burst traffic while maintaining average rate. + +```php +#[RateLimit(algorithm: Algorithm::TOKEN_BUCKET)] +``` + +**Pros**: Allows burst traffic, flexible +**Cons**: Requires more configuration + +#### Leaky Bucket + +Processes requests at constant rate, queues bursts. + +```php +#[RateLimit(algorithm: Algorithm::LEAKY_BUCKET)] +``` + +**Pros**: Smooth output rate, prevents bursts +**Cons**: Can delay requests + +### Custom Rate Limiter + +You can implement your own rate limiter by implementing `RateLimiterInterface`: + +```php +index(); +} catch (FriendsOfHyperf\RateLimit\Exception\RateLimitException $e) { + // Rate limit exceeded + $message = $e->getMessage(); // "Too Many Attempts. Please try again in X seconds." + $code = $e->getCode(); // 429 +} +``` + +## Configuration + +The component uses Hyperf's Redis configuration. You can specify which Redis pool to use in the annotation or middleware: + +```php +// Using specific Redis pool +#[RateLimit(pool: 'rate_limit')] +``` + +Make sure to configure your Redis pool in `config/autoload/redis.php`: + +```php +return [ + 'default' => [ + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 6379), + 'auth' => env('REDIS_AUTH', null), + 'db' => 0, + 'pool' => [ + 'min_connections' => 1, + 'max_connections' => 30, + ], + ], + 'rate_limit' => [ + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 6379), + 'auth' => env('REDIS_AUTH', null), + 'db' => 1, + 'pool' => [ + 'min_connections' => 5, + 'max_connections' => 50, + ], + ], +]; +``` + +## Examples + +### Example 1: Login Rate Limiting + +Limit login attempts to prevent brute force attacks: + +```php +#[RateLimit( + key: 'login:{email}', + maxAttempts: 5, + decay: 300, // 5 minutes + response: 'Too many login attempts. Please try again after 5 minutes.', + responseCode: 429 +)] +public function login(string $email, string $password) +{ + // Login logic here +} +``` + +### Example 2: API Endpoint Rate Limit + +Different rate limits for different API endpoints: + +```php +class ApiController +{ + // Public API: 100 requests per minute + #[RateLimit(maxAttempts: 100, decay: 60)] + public function public() + { + // Public endpoint + } + + // Premium API: 1000 requests per minute + #[RateLimit(maxAttempts: 1000, decay: 60)] + public function premium() + { + // Premium endpoint + } +} +``` + +### Example 3: User-based Rate Limiting + +Rate limit per user: + +```php +#[RateLimit( + key: ['user', '{userId}', 'action'], + maxAttempts: 10, + decay: 3600 // 1 hour +)] +public function performAction(int $userId) +{ + // Action logic here +} +``` + +### Example 4: IP-based Rate Limiting + +Rate limit by IP address using middleware: + +```php +class IpRateLimitMiddleware extends RateLimitMiddleware +{ + protected function resolveKey(ServerRequestInterface $request): string + { + return 'ip:' . $this->getClientIp(); + } +} +``` + +## License + +[MIT](LICENSE) From aff8e5a3ba9127585646bb5b0fb1448252c0f83d Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:28:13 +0800 Subject: [PATCH 34/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20RE?= =?UTF-8?q?ADME=20=E4=B8=AD=E6=9B=B4=E6=96=B0=E9=99=90=E6=B5=81=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=E7=9A=84=E7=94=A8=E6=88=B7=20ID=20=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E7=AC=A6=EF=BC=8C=E4=BB=8E=20{user=5Fid}=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=20{userId}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rate-limit/README.md b/src/rate-limit/README.md index 18692b65f..b479c154b 100644 --- a/src/rate-limit/README.md +++ b/src/rate-limit/README.md @@ -80,7 +80,7 @@ class UserController * Custom key with user ID placeholder */ #[RateLimit( - key: 'user:{user_id}:action', + key: 'user:{userId}:action', maxAttempts: 10, decay: 3600 )] @@ -93,7 +93,7 @@ class UserController * Using array key */ #[RateLimit( - key: ['user', '{user_id}', 'create'], + key: ['user', '{userId}', 'create'], maxAttempts: 5, decay: 60 )] From ae6f69a0b9f35e049003d151a712e951b173a2d9 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:30:02 +0800 Subject: [PATCH 35/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20RE?= =?UTF-8?q?ADME=20=E4=B8=AD=E7=A7=BB=E9=99=A4=E7=A4=BA=E4=BE=8B=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E5=86=97=E4=BD=99=E9=83=A8=E5=88=86=EF=BC=8C?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E5=8A=A8=E6=80=81=E5=8D=A0=E4=BD=8D=E7=AC=A6?= =?UTF-8?q?=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/rate-limit/README.md b/src/rate-limit/README.md index b479c154b..9bbdf44a8 100644 --- a/src/rate-limit/README.md +++ b/src/rate-limit/README.md @@ -148,10 +148,6 @@ class UserController The `key` parameter supports dynamic placeholders that will be replaced with method arguments: ```php -// Method arguments: $userId, $action -#[RateLimit(key: 'user:{0}:{1}')] // Becomes: user:123:create -public function action($userId, $action) - // Named placeholders #[RateLimit(key: 'user:{userId}:{action}')] public function action($userId, $action) From 489488a9e43bde51e71c75f7d7f120e616d06c4f Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:37:57 +0800 Subject: [PATCH 36/39] =?UTF-8?q?=E6=B8=85=E7=90=86=EF=BC=9A=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=20ConfigProvider=20=E4=B8=AD=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E7=9A=84=E4=BE=9D=E8=B5=96=E5=92=8C=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/ConfigProvider.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/rate-limit/src/ConfigProvider.php b/src/rate-limit/src/ConfigProvider.php index f8867e3a3..7ab1473ed 100644 --- a/src/rate-limit/src/ConfigProvider.php +++ b/src/rate-limit/src/ConfigProvider.php @@ -18,20 +18,9 @@ class ConfigProvider public function __invoke(): array { return [ - 'dependencies' => [ - // Add dependencies here - ], 'aspects' => [ RateLimitAspect::class, ], - 'publish' => [ - [ - 'id' => 'config', - 'description' => 'The configuration file of rate-limit.', - 'source' => __DIR__ . '/../publish/rate_limit.php', - 'destination' => BASE_PATH . '/config/autoload/rate_limit.php', - ], - ], ]; } } From d0abe587cd9620f41aa484dac4d6f405c1a4649e Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:40:02 +0800 Subject: [PATCH 37/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E4=B8=BA=20Ra?= =?UTF-8?q?teLimiterFactory=20=E7=B1=BB=E4=B8=AD=E7=9A=84=20limiters=20?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E6=B7=BB=E5=8A=A0=E7=BC=BA=E5=A4=B1=E7=9A=84?= =?UTF-8?q?=20PHPDoc=20=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/RateLimiterFactory.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rate-limit/src/RateLimiterFactory.php b/src/rate-limit/src/RateLimiterFactory.php index 2c8963a74..74d9ac4a7 100644 --- a/src/rate-limit/src/RateLimiterFactory.php +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -22,6 +22,9 @@ class RateLimiterFactory { + /** + * @var array + */ protected array $limiters = []; public function __construct(protected ContainerInterface $container) From f83d3c7ed663827dcb5a0ccceeae97c403cb72b2 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:19:02 +0800 Subject: [PATCH 38/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Lu?= =?UTF-8?q?aScripts=20=E4=B8=AD=E4=B8=BA=20zadd=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9A=8F=E6=9C=BA=E6=95=B0=E4=BB=A5=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E5=94=AF=E4=B8=80=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Storage/LuaScripts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit/src/Storage/LuaScripts.php b/src/rate-limit/src/Storage/LuaScripts.php index 0bf756e3b..6ce25ac92 100644 --- a/src/rate-limit/src/Storage/LuaScripts.php +++ b/src/rate-limit/src/Storage/LuaScripts.php @@ -69,7 +69,7 @@ public static function slidingWindow(): string end -- Add current request -redis.call('zadd', key, current_time, current_time) +redis.call('zadd', key, current_time, current_time .. ':' .. math.random()) redis.call('expire', key, decay + 1) return {1, current + 1, decay} From fd5cd6d7f8c3c914c48be63f46139eb9edc07db6 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:20:02 +0800 Subject: [PATCH 39/39] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=9C=A8=20Lu?= =?UTF-8?q?aScripts=20=E4=B8=AD=E4=B8=BA=20zadd=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9A=8F=E6=9C=BA=E6=95=B0=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E4=BB=A5=E7=A1=AE=E4=BF=9D=E5=94=AF=E4=B8=80=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/rate-limit/src/Storage/LuaScripts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rate-limit/src/Storage/LuaScripts.php b/src/rate-limit/src/Storage/LuaScripts.php index 6ce25ac92..91307d454 100644 --- a/src/rate-limit/src/Storage/LuaScripts.php +++ b/src/rate-limit/src/Storage/LuaScripts.php @@ -69,7 +69,7 @@ public static function slidingWindow(): string end -- Add current request -redis.call('zadd', key, current_time, current_time .. ':' .. math.random()) +redis.call('zadd', key, current_time, current_time .. ':' .. math.random(1000000, 9999999)) redis.call('expire', key, decay + 1) return {1, current + 1, decay}