From 1c11eb9a4381466e0c6767269c06812e0048d7c7 Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Wed, 25 Mar 2026 23:18:59 +0000 Subject: [PATCH 1/3] feature: handle exceptions generating cache values closes #13 --- README.md | 4 ++++ example/01-latlon.php | 21 +++++++++++++++- src/Cache.php | 11 ++++++--- test/phpunit/CacheTest.php | 49 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cc3beeb..b78f0ff 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ $location = $fileCache->get("lat-lon", $lookup); echo "Your location is: $location"; ``` +If generating a fresh value fails, throw `Gt\FileCache\CacheValueGenerationException` +from the callback. The cache will ignore that failure and skip writing a replacement +value, which leaves `null` available as a legitimate cached value. + # Proudly sponsored by [JetBrains Open Source sponsorship program](https://www.jetbrains.com/community/opensource/) diff --git a/example/01-latlon.php b/example/01-latlon.php index 3943ad3..e71e16b 100644 --- a/example/01-latlon.php +++ b/example/01-latlon.php @@ -17,7 +17,26 @@ function httpJson(string $uri):object { $ch = curl_init($uri); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - return json_decode(curl_exec($ch)); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $response = curl_exec($ch); + if($response === false) { + throw new Gt\FileCache\CacheValueGenerationException(curl_error($ch)); + } + + try { + return json_decode($response, flags: JSON_THROW_ON_ERROR); + } + catch(JsonException $exception) { + throw new Gt\FileCache\CacheValueGenerationException( + "Invalid JSON returned from $uri", + previous: $exception, + ); + } + finally { + curl_close($ch); + } } $ipAddress = $fileCache->get("ip", function():string { diff --git a/src/Cache.php b/src/Cache.php index 2e07524..1986d7c 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -29,9 +29,14 @@ public function get( return $this->fileAccess->getData($name); } catch(FileNotFoundException|CacheInvalidException) { - $value = $callback(); - $this->fileAccess->setData($name, $value); - return $value; + try { + $value = $callback(); + $this->fileAccess->setData($name, $value); + return $value; + } + catch(CacheValueGenerationException) { + return null; + } } } diff --git a/test/phpunit/CacheTest.php b/test/phpunit/CacheTest.php index 20f0e87..d6d8f75 100644 --- a/test/phpunit/CacheTest.php +++ b/test/phpunit/CacheTest.php @@ -2,7 +2,10 @@ namespace Gt\FileCache\Test; use Gt\FileCache\Cache; +use Gt\FileCache\CacheInvalidException; +use Gt\FileCache\CacheValueGenerationException; use Gt\FileCache\FileAccess; +use Gt\FileCache\FileNotFoundException; use PHPUnit\Framework\TestCase; use SplFileInfo; use SplFixedArray; @@ -49,6 +52,52 @@ public function testGet_multipleCallsDoesNotCallbackMultipleTimes():void { self::assertSame(1, $count); } + public function testGet_nullValueCanBeCached():void { + $sut = $this->getSut(); + $count = 0; + + $callback = function()use(&$count):null { + $count++; + return null; + }; + + self::assertNull($sut->get("test-null", $callback)); + self::assertNull($sut->get("test-null", $callback)); + self::assertSame(1, $count); + } + + public function testGet_generationExceptionDoesNotWriteInvalidValue():void { + $fileAccess = self::createMock(FileAccess::class); + $fileAccess->expects(self::once()) + ->method("checkValidity") + ->with("test", 3600) + ->willThrowException(new FileNotFoundException("test")); + $fileAccess->expects(self::never()) + ->method("setData"); + + $sut = new Cache(fileAccess: $fileAccess); + + self::assertNull($sut->get("test", function():never { + throw new CacheValueGenerationException("Lookup failed"); + })); + } + + public function testGet_invalidCache_generationExceptionDoesNotWriteReplacement():void { + $fileAccess = self::createMock(FileAccess::class); + $fileAccess->expects(self::once()) + ->method("checkValidity") + ->with("test", 3600) + ->willThrowException(new CacheInvalidException("test")); + $fileAccess->expects(self::never()) + ->method("setData"); + + $sut = new Cache(fileAccess: $fileAccess); + + self::assertNull($sut->get("test", function():never { + throw new CacheValueGenerationException("Lookup failed"); + })); + } + public function testGetString():void { $value = uniqid(); $sut = $this->getSut([ From b6d2fee2c2c67cce767ea890ef7fa4ea041b400f Mon Sep 17 00:00:00 2001 From: Greg Bowler Date: Fri, 27 Mar 2026 12:04:05 +0000 Subject: [PATCH 2/3] feature: handle exceptions generating cache values closes #13 --- src/CacheValueGenerationException.php | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/CacheValueGenerationException.php diff --git a/src/CacheValueGenerationException.php b/src/CacheValueGenerationException.php new file mode 100644 index 0000000..e28e31f --- /dev/null +++ b/src/CacheValueGenerationException.php @@ -0,0 +1,4 @@ + Date: Fri, 27 Mar 2026 12:06:06 +0000 Subject: [PATCH 3/3] ci: codacy --- .codacy.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .codacy.yml diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..f27de9c --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +exclude_paths: + - ".github/**" + - "example/**" + - "test/**" +