From 41417330c88d05e2e4a790e7df111bce8ad8b9bc Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Wed, 14 Jan 2026 14:44:38 -0500 Subject: [PATCH 1/4] fix: catch cache error when cache is unavailable --- src/ToggleManager.php | 40 ++++++++++++++++++++++++++----------- tests/Feature/CacheTest.php | 26 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/ToggleManager.php b/src/ToggleManager.php index 9d4fa6f..685500e 100644 --- a/src/ToggleManager.php +++ b/src/ToggleManager.php @@ -10,6 +10,7 @@ use OffloadProject\Toggle\Contracts\Driver; use OffloadProject\Toggle\Exceptions\ToggleNotFoundException; use RuntimeException; +use Throwable; class ToggleManager { @@ -33,11 +34,16 @@ public function active(string|BackedEnum $name): bool $key = $this->normalizeKey($name); if ($this->cacheEnabled()) { - return $this->getCache()->remember( - $this->cacheKey($key), - $this->cacheTtl(), - fn () => $this->resolve($key) - ); + try { + return $this->getCache()->remember( + $this->cacheKey($key), + $this->cacheTtl(), + fn () => $this->resolve($key) + ); + } catch (Throwable) { + // Cache unavailable (e.g., database doesn't exist yet during boot) + // Fall through to resolve without cache + } } return $this->resolve($key); @@ -113,7 +119,12 @@ public function forgetCache(string|BackedEnum $name): bool { $key = $this->normalizeKey($name); - return $this->getCache()->forget($this->cacheKey($key)); + try { + return $this->getCache()->forget($this->cacheKey($key)); + } catch (Throwable) { + // Cache unavailable + return false; + } } /** @@ -121,14 +132,19 @@ public function forgetCache(string|BackedEnum $name): bool */ public function flushCache(): bool { - $cache = $this->getCache(); + try { + $cache = $this->getCache(); - // Clear all known toggles from cache - foreach ($this->getDriver()->all() as $name => $value) { - $cache->forget($this->cacheKey($name)); - } + // Clear all known toggles from cache + foreach ($this->getDriver()->all() as $name => $value) { + $cache->forget($this->cacheKey($name)); + } - return true; + return true; + } catch (Throwable) { + // Cache unavailable + return false; + } } /** diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index c805876..c90c371 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -39,3 +39,29 @@ expect(Cache::has('toggle:test-flag'))->toBeFalse(); expect(Cache::has('toggle:disabled-flag'))->toBeFalse(); }); + +it('gracefully handles cache unavailability during active check', function () { + // Mock cache to throw an exception (simulating database not existing) + Cache::shouldReceive('store') + ->andThrow(new RuntimeException('Database does not exist')); + + // Should still resolve the toggle without cache + expect(Toggle::active('test-flag'))->toBeTrue(); + expect(Toggle::active('disabled-flag'))->toBeFalse(); +}); + +it('gracefully handles cache unavailability during forgetCache', function () { + Cache::shouldReceive('store') + ->andThrow(new RuntimeException('Database does not exist')); + + // Should return false but not throw + expect(Toggle::forgetCache('test-flag'))->toBeFalse(); +}); + +it('gracefully handles cache unavailability during flushCache', function () { + Cache::shouldReceive('store') + ->andThrow(new RuntimeException('Database does not exist')); + + // Should return false but not throw + expect(Toggle::flushCache())->toBeFalse(); +}); From 398fc880c6d4cee35b304c6bb1e313e7e374d2fc Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Wed, 14 Jan 2026 16:15:33 -0500 Subject: [PATCH 2/4] fix: check for specific exceptions --- src/ToggleManager.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ToggleManager.php b/src/ToggleManager.php index 685500e..5738c40 100644 --- a/src/ToggleManager.php +++ b/src/ToggleManager.php @@ -40,9 +40,11 @@ public function active(string|BackedEnum $name): bool $this->cacheTtl(), fn () => $this->resolve($key) ); - } catch (Throwable) { - // Cache unavailable (e.g., database doesn't exist yet during boot) - // Fall through to resolve without cache + } catch (Throwable $exception) { + // Rethrow non-cache-related exceptions so that genuine bugs are not masked. + if ($exception instanceof ToggleNotFoundException || $exception instanceof RuntimeException) { + throw $exception; + } } } From c99d95a0d54376049c08106195051d286cc3a5de Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Wed, 14 Jan 2026 17:07:59 -0500 Subject: [PATCH 3/4] fix: check for specific exceptions --- src/ToggleManager.php | 6 ++++-- tests/Feature/CacheTest.php | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ToggleManager.php b/src/ToggleManager.php index 5738c40..cbe8f5e 100644 --- a/src/ToggleManager.php +++ b/src/ToggleManager.php @@ -41,10 +41,12 @@ public function active(string|BackedEnum $name): bool fn () => $this->resolve($key) ); } catch (Throwable $exception) { - // Rethrow non-cache-related exceptions so that genuine bugs are not masked. - if ($exception instanceof ToggleNotFoundException || $exception instanceof RuntimeException) { + // Rethrow toggle-specific exceptions so that genuine bugs are not masked. + if ($exception instanceof ToggleNotFoundException) { throw $exception; } + // Other exceptions (QueryException, PDOException, etc.) indicate cache + // unavailability - fall through to resolve without cache } } diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index c90c371..5ce82d7 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Cache; use OffloadProject\Toggle\Facades\Toggle; @@ -41,9 +42,10 @@ }); it('gracefully handles cache unavailability during active check', function () { - // Mock cache to throw an exception (simulating database not existing) + // Mock cache to throw a QueryException (simulating database not existing) + $exception = new QueryException('sqlite', 'select * from cache', [], new Exception('Database does not exist')); Cache::shouldReceive('store') - ->andThrow(new RuntimeException('Database does not exist')); + ->andThrow($exception); // Should still resolve the toggle without cache expect(Toggle::active('test-flag'))->toBeTrue(); @@ -51,16 +53,18 @@ }); it('gracefully handles cache unavailability during forgetCache', function () { + $exception = new QueryException('sqlite', 'select * from cache', [], new Exception('Database does not exist')); Cache::shouldReceive('store') - ->andThrow(new RuntimeException('Database does not exist')); + ->andThrow($exception); // Should return false but not throw expect(Toggle::forgetCache('test-flag'))->toBeFalse(); }); it('gracefully handles cache unavailability during flushCache', function () { + $exception = new QueryException('sqlite', 'select * from cache', [], new Exception('Database does not exist')); Cache::shouldReceive('store') - ->andThrow(new RuntimeException('Database does not exist')); + ->andThrow($exception); // Should return false but not throw expect(Toggle::flushCache())->toBeFalse(); From c9f2cccef999008be60d54ec5805080d6c2d36fb Mon Sep 17 00:00:00 2001 From: Shavonn Brown Date: Wed, 14 Jan 2026 22:07:16 -0500 Subject: [PATCH 4/4] fix: pr comments --- src/ToggleManager.php | 6 ++++-- tests/Feature/CacheTest.php | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ToggleManager.php b/src/ToggleManager.php index cbe8f5e..629716c 100644 --- a/src/ToggleManager.php +++ b/src/ToggleManager.php @@ -136,11 +136,13 @@ public function forgetCache(string|BackedEnum $name): bool */ public function flushCache(): bool { + // Get toggles from driver first (let driver exceptions propagate) + $toggles = $this->getDriver()->all(); + try { $cache = $this->getCache(); - // Clear all known toggles from cache - foreach ($this->getDriver()->all() as $name => $value) { + foreach ($toggles as $name => $value) { $cache->forget($this->cacheKey($name)); } diff --git a/tests/Feature/CacheTest.php b/tests/Feature/CacheTest.php index 5ce82d7..e8cb1a9 100644 --- a/tests/Feature/CacheTest.php +++ b/tests/Feature/CacheTest.php @@ -42,9 +42,13 @@ }); it('gracefully handles cache unavailability during active check', function () { + // Verify caching is enabled for this test + expect(config('toggle.cache.enabled'))->toBeTrue(); + // Mock cache to throw a QueryException (simulating database not existing) $exception = new QueryException('sqlite', 'select * from cache', [], new Exception('Database does not exist')); Cache::shouldReceive('store') + ->atLeast()->once() ->andThrow($exception); // Should still resolve the toggle without cache @@ -55,6 +59,7 @@ it('gracefully handles cache unavailability during forgetCache', function () { $exception = new QueryException('sqlite', 'select * from cache', [], new Exception('Database does not exist')); Cache::shouldReceive('store') + ->once() ->andThrow($exception); // Should return false but not throw @@ -64,6 +69,7 @@ it('gracefully handles cache unavailability during flushCache', function () { $exception = new QueryException('sqlite', 'select * from cache', [], new Exception('Database does not exist')); Cache::shouldReceive('store') + ->once() ->andThrow($exception); // Should return false but not throw