diff --git a/composer.json b/composer.json index 222ea3af0..ea88bcd63 100644 --- a/composer.json +++ b/composer.json @@ -153,6 +153,7 @@ "friendsofhyperf/openai-client": "*", "friendsofhyperf/pretty-console": "*", "friendsofhyperf/purifier": "*", + "friendsofhyperf/rate-limit": "*", "friendsofhyperf/recaptcha": "*", "friendsofhyperf/redis-subscriber": "*", "friendsofhyperf/sentry": "*", @@ -206,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/", @@ -286,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/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-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/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)| 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/.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/.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 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..9bbdf44a8 --- /dev/null +++ b/src/rate-limit/README.md @@ -0,0 +1,406 @@ +# 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) diff --git a/src/rate-limit/composer.json b/src/rate-limit/composer.json new file mode 100644 index 000000000..804d6f083 --- /dev/null +++ b/src/rate-limit/composer.json @@ -0,0 +1,54 @@ +{ + "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/collection": "~3.1.0", + "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/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 @@ +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 (is_int($ttl) && $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..1ed0755e5 --- /dev/null +++ b/src/rate-limit/src/Algorithm/LeakyBucketRateLimiter.php @@ -0,0 +1,69 @@ +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 (is_int($ttl) && $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..c64f5ec0d --- /dev/null +++ b/src/rate-limit/src/Algorithm/SlidingWindowRateLimiter.php @@ -0,0 +1,66 @@ +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 (is_int($ttl) && $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..1569633bd --- /dev/null +++ b/src/rate-limit/src/Algorithm/TokenBucketRateLimiter.php @@ -0,0 +1,69 @@ +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 (is_int($ttl) && $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..b55c74ada --- /dev/null +++ b/src/rate-limit/src/Annotation/RateLimit.php @@ -0,0 +1,40 @@ +getAnnotationMetadata(); + + /** @var null|RateLimit $annotation */ + $annotation = $metadata->method[RateLimit::class] ?? null; + + if (! $annotation) { + return $proceedingJoinPoint->process(); + } + + $key = $this->resolveKey($annotation->key, $proceedingJoinPoint); + $limiter = $this->factory->make($annotation->algorithm, $annotation->pool); + + 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 + ), + $annotation->responseCode + ); + } + + return $proceedingJoinPoint->process(); + } + + protected function resolveKey(string|array $key, ProceedingJoinPoint $proceedingJoinPoint): string + { + if (empty($key)) { + // Use method signature as default key + $className = $proceedingJoinPoint->className; + $methodName = $proceedingJoinPoint->methodName; + return "{$className}:{$methodName}"; + } + + if (is_callable($key)) { + return $key($proceedingJoinPoint); + } + + if (is_array($key)) { + $key = implode(':', array_values($key)); + } + + // Support placeholders like {user_id}, {ip}, etc. + if (str_contains($key, '{')) { + $key = preg_replace_callback('/\{([^}]+)\}/', function ($matches) use ($proceedingJoinPoint) { + $placeholder = $matches[1]; + + /** @var array $arguments */ + $arguments = $proceedingJoinPoint->arguments['keys'] ?? []; + foreach ($arguments as $argKey => $argValue) { + if ( + (is_array($argValue) || is_object($argValue)) + && str_contains($placeholder, '.') + ) { + return (string) data_get($arguments, $placeholder); + } + if ($argKey === $placeholder) { + return (string) $argValue; + } + } + + return $matches[0]; + }, $key); + } + + return $key; + } +} diff --git a/src/rate-limit/src/ConfigProvider.php b/src/rate-limit/src/ConfigProvider.php new file mode 100644 index 000000000..7ab1473ed --- /dev/null +++ b/src/rate-limit/src/ConfigProvider.php @@ -0,0 +1,26 @@ + [ + RateLimitAspect::class, + ], + ]; + } +} 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, $this->pool); + + 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 + ->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)); + } + + /** + * Add rate limit headers to response. + */ + protected function addHeaders( + ResponseInterface $response, + int $maxAttempts, + int $remaining, + int $retryAfter + ): ResponseInterface { + return $response + ->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 new file mode 100644 index 000000000..74d9ac4a7 --- /dev/null +++ b/src/rate-limit/src/RateLimiterFactory.php @@ -0,0 +1,62 @@ + + */ + protected array $limiters = []; + + public function __construct(protected ContainerInterface $container) + { + } + + public function make(Algorithm $algorithm = Algorithm::FIXED_WINDOW, ?string $pool = null): RateLimiterInterface + { + $key = $algorithm->value . ':' . ($pool ?? 'default'); + + if (isset($this->limiters[$key])) { + return $this->limiters[$key]; + } + + $redis = $this->getRedis($pool); + $prefix = $this->getPrefix(); + + return $this->limiters[$key] = match ($algorithm) { + 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), + }; + } + + protected function getRedis(?string $pool = null): Redis + { + return $this->container->get(RedisFactory::class)->get($pool ?? 'default'); + } + + 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..91307d454 --- /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 .. ':' .. math.random(1000000, 9999999)) +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; + } +} 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..f0eaffbe2 --- /dev/null +++ b/tests/RateLimit/AnnotationTest.php @@ -0,0 +1,47 @@ +key)->toBe(''); + expect($annotation->maxAttempts)->toBe(60); + expect($annotation->decay)->toBe(60); + expect($annotation->algorithm)->toBe(Algorithm::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: 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(Algorithm::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..d11ac7624 --- /dev/null +++ b/tests/RateLimit/RateLimiterFactoryTest.php @@ -0,0 +1,109 @@ +shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make(Algorithm::FIXED_WINDOW); + + expect($limiter)->toBeInstanceOf(Algorithm\FixedWindowRateLimiter::class); +}); + +test('factory can create sliding window limiter', function () { + $container = m::mock(ContainerInterface::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); + + $container->shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make(Algorithm::SLIDING_WINDOW); + + expect($limiter)->toBeInstanceOf(Algorithm\SlidingWindowRateLimiter::class); +}); + +test('factory can create token bucket limiter', function () { + $container = m::mock(ContainerInterface::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); + + $container->shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make(Algorithm::TOKEN_BUCKET); + + expect($limiter)->toBeInstanceOf(Algorithm\TokenBucketRateLimiter::class); +}); + +test('factory can create leaky bucket limiter', function () { + $container = m::mock(ContainerInterface::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); + + $container->shouldReceive('get') + ->with(RedisFactory::class) + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter = $factory->make(Algorithm::LEAKY_BUCKET); + + expect($limiter)->toBeInstanceOf(Algorithm\LeakyBucketRateLimiter::class); +}); + +test('factory caches limiter instances', function () { + $container = m::mock(ContainerInterface::class); + $redisFactory = m::mock(RedisFactory::class); + $redis = m::mock(RedisProxy::class); + + $container->shouldReceive('get') + ->with(RedisFactory::class) + ->once() + ->andReturn($redisFactory); + $redisFactory->shouldReceive('get') + ->with('default') + ->once() + ->andReturn($redis); + + $factory = new RateLimiterFactory($container); + $limiter1 = $factory->make(Algorithm::FIXED_WINDOW); + $limiter2 = $factory->make(Algorithm::FIXED_WINDOW); + + expect($limiter1)->toBe($limiter2); +});