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([