Merge pull request #21 from trypostit/hotfix/has-usage-int-cast

fix: cast cached post count to int (Redis serializer crash)
This commit is contained in:
Paulo Castellano 2026-05-07 14:32:09 -03:00 committed by GitHub
commit e653c900db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 30 additions and 2 deletions

View file

@ -66,6 +66,15 @@ public function featureLimits(): array
}
/**
* The `(int)` cast on the cached value is load-bearing: Laravel's
* RedisStore skips serialize()/unserialize() for is_numeric values so
* they can be incremented atomically — the side effect is that an int
* stored via `Cache::put` comes back as a string on read. Without the
* cast, the strict `: int` return type throws a TypeError under the
* Redis cache driver (production). File/array/database drivers don't
* have this optimisation and preserve the type, which is why the bug
* never surfaced in tests or local dev.
*
* @param array<int, string> $workspaceIds
*/
private function cachedPostCount(array $workspaceIds): int
@ -74,7 +83,7 @@ private function cachedPostCount(array $workspaceIds): int
return 0;
}
return Cache::remember(
return (int) Cache::remember(
"account:{$this->id}:posts_count",
self::POST_COUNT_CACHE_TTL,
fn () => Post::whereIn('workspace_id', $workspaceIds)->count(),

View file

@ -17,7 +17,7 @@
|
*/
'default' => env('CACHE_STORE', 'database'),
'default' => env('CACHE_STORE', 'redis'),
/*
|--------------------------------------------------------------------------

View file

@ -104,3 +104,22 @@
// No cache entry should be written for the empty case.
expect(Cache::has("account:{$this->account->id}:posts_count"))->toBeFalse();
});
test('postCount survives a string-typed cache value (Redis serializer quirk)', function () {
// Laravel's RedisStore stores numeric values raw (not serialised) so they
// can be INCRemented atomically. The side effect: an int written via
// Cache::put comes back as a string on read. The test driver is `array`
// which preserves type, so we seed the cache with a literal string here
// to mimic what production sees and assert the return type stays int.
Workspace::factory()->create([
'account_id' => $this->account->id,
'user_id' => $this->owner->id,
]);
Cache::put("account:{$this->account->id}:posts_count", '42', 300);
$usage = $this->account->usage();
expect($usage['postCount'])->toBe(42);
expect($usage['postCount'])->toBeInt();
});