From 175a20100496b46952f0226385880d17d0ed5cbb Mon Sep 17 00:00:00 2001 From: Basil Date: Wed, 19 Aug 2020 12:57:45 +0200 Subject: [PATCH] Fix #18236: Allow `yii\filters\RateLimiter` to accept a closure function for the `$user` property in order to assign values on runtime --- framework/CHANGELOG.md | 1 + framework/filters/RateLimiter.php | 15 +++++++++++++-- tests/framework/filters/RateLimiterTest.php | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index acc5ceafcc..d28e1d3337 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.38 under development ------------------------ +- Enh #18236: Allow `yii\filters\RateLimiter` to accept a closure function for the `$user` property in order to assign values on runtime (nadar) - Bug #18233: Add PHP 8 support (samdark) - Bug #18229: Add flag for recognize SyBase databases on uses pdo_dblib (darkdef) diff --git a/framework/filters/RateLimiter.php b/framework/filters/RateLimiter.php index dea1d48079..daa8310d26 100644 --- a/framework/filters/RateLimiter.php +++ b/framework/filters/RateLimiter.php @@ -7,6 +7,7 @@ namespace yii\filters; +use Closure; use Yii; use yii\base\ActionFilter; use yii\web\Request; @@ -48,8 +49,14 @@ class RateLimiter extends ActionFilter */ public $errorMessage = 'Rate limit exceeded.'; /** - * @var RateLimitInterface the user object that implements the RateLimitInterface. - * If not set, it will take the value of `Yii::$app->user->getIdentity(false)`. + * @var RateLimitInterface|Closure the user object that implements the RateLimitInterface. If not set, it will take the value of `Yii::$app->user->getIdentity(false)`. + * {@since 2.0.38} It's possible to provide a closure function in order to assign the user identity on runtime. Using a closure to assign the user identity is recommend + * when you are **not** using the standard `Yii::$app->user` component. See the example below: + * ```php + * 'user' => function() { + * return Yii::$app->apiUser->identity; + * } + * ``` */ public $user; /** @@ -84,6 +91,10 @@ class RateLimiter extends ActionFilter $this->user = Yii::$app->getUser()->getIdentity(false); } + if ($this->user instanceof Closure) { + $this->user = call_user_func($this->user, $action); + } + if ($this->user instanceof RateLimitInterface) { Yii::debug('Check rate limit', __METHOD__); $this->checkRateLimit($this->user, $this->request, $this->response, $action); diff --git a/tests/framework/filters/RateLimiterTest.php b/tests/framework/filters/RateLimiterTest.php index 3126176373..f13c4ec13c 100644 --- a/tests/framework/filters/RateLimiterTest.php +++ b/tests/framework/filters/RateLimiterTest.php @@ -158,4 +158,21 @@ class RateLimiterTest extends TestCase $rateLimiter->addRateLimitHeaders($response, 1, 0, 0); $this->assertCount(3, $response->getHeaders()); } + + /** + * @see https://github.com/yiisoft/yii2/issues/18236 + */ + public function testUserWithClosureFunction() + { + $rateLimiter = new RateLimiter(); + $rateLimiter->user = function($action) { + return new User(['identityClass' => RateLimit::className()]); + }; + $rateLimiter->beforeAction('test'); + + // testing the evaluation of user closure, which in this case returns not the expect object and therefore + // the log message "does not implement RateLimitInterface" is expected. + $this->assertInstanceOf(User::className(), $rateLimiter->user); + $this->assertContains('Rate limit skipped: "user" does not implement RateLimitInterface.', Yii::getLogger()->messages); + } }