Skip to content

Commit 64b4d15

Browse files
authored
Merge pull request #123 from phenixphp/develop
Release v0.8.5
2 parents 9eb5921 + 4c939b4 commit 64b4d15

7 files changed

Lines changed: 94 additions & 14 deletions

File tree

src/Cache/RateLimit/RateLimitConfig.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ public function connection(): string
2525
return $this->config['connection'] ?? 'default';
2626
}
2727

28+
public function prefix(): string
29+
{
30+
return (string) Configuration::get('cache.prefix', '');
31+
}
32+
2833
public function ttl(): int
2934
{
3035
return 60;

src/Cache/RateLimit/RateLimitFactory.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
class RateLimitFactory
1616
{
17-
public static function redis(int $ttl, string $connection = 'default'): RateLimit
17+
public static function redis(int $ttl, string $connection = 'default', string $prefix = ''): RateLimit
1818
{
1919
$clientWrapper = Redis::connection($connection)->client();
20+
$rateLimit = new RedisRateLimit($clientWrapper->getClient(), $ttl);
2021

21-
return new RedisRateLimit($clientWrapper->getClient(), $ttl);
22+
return self::withPrefix($rateLimit, $prefix);
2223
}
2324

2425
public static function local(int $ttl): RateLimit
@@ -31,6 +32,10 @@ public static function local(int $ttl): RateLimit
3132

3233
public static function withPrefix(RateLimit $rateLimit, string $prefix): RateLimit
3334
{
35+
if ($prefix === '') {
36+
return $rateLimit;
37+
}
38+
3439
return new PrefixRateLimit($rateLimit, $prefix);
3540
}
3641
}

src/Cache/RateLimit/RateLimitManager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function prefixed(string $prefix): self
4747
protected function resolveStore(): RateLimit
4848
{
4949
return match ($this->config->default()) {
50-
'redis' => RateLimitFactory::redis($this->config->ttl(), $this->config->connection()),
50+
'redis' => RateLimitFactory::redis($this->config->ttl(), $this->config->connection(), $this->config->prefix()),
5151
'local' => RateLimitFactory::local($this->config->ttl()),
5252
default => RateLimitFactory::local($this->config->ttl()),
5353
};

src/Cache/Stores/RedisStore.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public function clear(): void
5959
$iterator = null;
6060

6161
do {
62-
[$keys, $iterator] = $this->client->execute('SCAN', $iterator ?? 0, 'MATCH', $this->getPrefixedKey('*'), 'COUNT', 1000);
62+
[$iterator, $keys] = $this->client->execute('SCAN', $iterator ?? 0, 'MATCH', $this->getPrefixedKey('*'), 'COUNT', 1000);
6363

6464
if (! empty($keys)) {
6565
$this->client->execute('DEL', ...$keys);

src/Testing/TestCase.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
use Phenix\Cache\Constants\Store;
1313
use Phenix\Console\Phenix;
1414
use Phenix\Facades\Cache;
15+
use Phenix\Facades\Config;
1516
use Phenix\Facades\Event;
1617
use Phenix\Facades\Mail;
1718
use Phenix\Facades\Queue;
1819
use Phenix\Facades\View;
1920
use Phenix\Testing\Concerns\InteractWithDatabase;
2021
use Phenix\Testing\Concerns\InteractWithResponses;
2122
use Phenix\Testing\Concerns\RefreshDatabase;
23+
use Phenix\Util\Str;
2224
use Symfony\Component\Console\Tester\CommandTester;
2325
use Throwable;
2426

@@ -45,6 +47,8 @@ protected function setUp(): void
4547
$this->app->enableTestingMode();
4648
}
4749

50+
Config::set('cache.prefix', sprintf('phenix_test_%s_', Str::random(16)));
51+
4852
$uses = class_uses_recursive($this);
4953

5054
if (in_array(RefreshDatabase::class, $uses, true) && method_exists($this, 'refreshDatabase')) {

tests/Unit/Cache/RateLimit/RedisRateLimitTest.php

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,79 @@
22

33
declare(strict_types=1);
44

5+
use Amp\Redis\Connection\RedisLink;
6+
use Amp\Redis\Protocol\RedisResponse;
7+
use Amp\Redis\RedisClient;
8+
use Kelunik\RateLimit\PrefixRateLimit;
59
use Kelunik\RateLimit\RedisRateLimit;
610
use Phenix\Cache\Constants\Store;
711
use Phenix\Cache\RateLimit\RateLimitManager;
12+
use Phenix\Database\Constants\Connection;
813
use Phenix\Facades\Config;
14+
use Phenix\Redis\ClientWrapper;
915

1016
beforeEach(function (): void {
1117
Config::set('cache.default', Store::REDIS->value);
1218
Config::set('cache.rate_limit.store', Store::REDIS->value);
1319
});
1420

15-
it('call redis rate limit factory', function (): void {
21+
it('prefixes redis rate limit keys with the cache namespace', function (): void {
22+
Config::set('cache.prefix', 'cache-prefix:');
23+
1624
$manager = new RateLimitManager();
25+
$limiter = $manager->limiter();
26+
27+
expect($limiter)->toBeInstanceOf(PrefixRateLimit::class);
28+
29+
$reflection = new ReflectionClass($limiter);
30+
31+
$prefix = $reflection->getProperty('prefix');
32+
$prefix->setAccessible(true);
33+
34+
$rateLimit = $reflection->getProperty('rateLimit');
35+
$rateLimit->setAccessible(true);
36+
37+
expect($prefix->getValue($limiter))->toBe('cache-prefix:');
38+
expect($rateLimit->getValue($limiter))->toBeInstanceOf(RedisRateLimit::class);
39+
});
40+
41+
it('isolates redis rate limit state across cache prefixes', function (): void {
42+
$incrementResponse = $this->createStub(RedisResponse::class);
43+
$incrementResponse->method('unwrap')->willReturn(1);
44+
45+
$expireResponse = $this->createStub(RedisResponse::class);
46+
$expireResponse->method('unwrap')->willReturn(1);
47+
48+
$getResponse = $this->createStub(RedisResponse::class);
49+
$getResponse->method('unwrap')->willReturn(null);
50+
51+
$link = $this->createMock(RedisLink::class);
52+
53+
$link->expects($this->exactly(3))
54+
->method('execute')
55+
->withConsecutive(
56+
[
57+
$this->equalTo('incr'),
58+
$this->equalTo(['first-prefix:route:client']),
59+
],
60+
[
61+
$this->equalTo('expire'),
62+
$this->equalTo(['first-prefix:route:client', 60]),
63+
],
64+
[
65+
$this->equalTo('get'),
66+
$this->equalTo(['second-prefix:route:client']),
67+
]
68+
)
69+
->willReturnOnConsecutiveCalls($incrementResponse, $expireResponse, $getResponse);
70+
71+
$client = new RedisClient($link);
72+
$this->app->swap(Connection::redis('default'), new ClientWrapper($client));
73+
74+
Config::set('cache.prefix', 'first-prefix:');
75+
(new RateLimitManager())->prefixed('route:')->increment('client');
76+
77+
Config::set('cache.prefix', 'second-prefix:');
1778

18-
expect($manager->limiter())->toBeInstanceOf(RedisRateLimit::class);
79+
expect((new RateLimitManager())->prefixed('route:')->get('client'))->toBe(0);
1980
});

tests/Unit/Cache/RedisStoreTest.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
expect(Cache::has('gamma'))->toBeFalse();
166166
});
167167

168-
it('clears all values', function (): void {
168+
it('clears all values across scan iterations', function (): void {
169169
$client = $this->getMockBuilder(ClientWrapper::class)
170170
->disableOriginalConstructor()
171171
->getMock();
@@ -190,21 +190,28 @@
190190
expect($args[4])->toBe('COUNT');
191191
expect($args[5])->toBe(1000);
192192

193-
return [["{$prefix}a", "{$prefix}b"], '0'];
193+
return ['12', []];
194194
}
195195

196196
if ($callCount === 4) {
197+
expect($args[0])->toBe('SCAN');
198+
expect($args[1])->toBe('12');
199+
expect($args[2])->toBe('MATCH');
200+
expect($args[3])->toBe("{$prefix}*");
201+
expect($args[4])->toBe('COUNT');
202+
expect($args[5])->toBe(1000);
203+
204+
return ['0', ["{$prefix}a", "{$prefix}b"]];
205+
}
206+
207+
if ($callCount === 5) {
197208
expect($args[0])->toBe('DEL');
198209
expect($args[1])->toBe("{$prefix}a");
199210
expect($args[2])->toBe("{$prefix}b");
200211

201212
return 2;
202213
}
203214

204-
if ($callCount === 5) {
205-
return 0;
206-
}
207-
208215
return null;
209216
});
210217

@@ -214,8 +221,6 @@
214221
Cache::set('b', 2);
215222

216223
Cache::clear();
217-
218-
expect(Cache::has('a'))->toBeFalse();
219224
});
220225

221226
it('stores forever without expiration', function (): void {

0 commit comments

Comments
 (0)