diff --git a/README.md b/README.md index b78f0ff..d0b0c90 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The first time we see the IP address will have to make an HTTP call, but subsequ ```php $ipAddress = $_SERVER["REMOTE_ADDR"]; -$fileCache = new Gt\FileCache\Cache("/tmp/ip-address-geolocation"); +$fileCache = new GT\FileCache\Cache("/tmp/ip-address-geolocation"); // This function uses file_get_contents to contact the remote server // at ipinfo.io, a costly operation. We will pass the lookup function @@ -44,7 +44,7 @@ $location = $fileCache->get("lat-lon", $lookup); echo "Your location is: $location"; ``` -If generating a fresh value fails, throw `Gt\FileCache\CacheValueGenerationException` +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. diff --git a/composer.json b/composer.json index b134d60..891e98f 100644 --- a/composer.json +++ b/composer.json @@ -40,13 +40,14 @@ "autoload": { "psr-4": { + "GT\\FileCache\\": "./src", "Gt\\FileCache\\": "./src" } }, "autoload-dev": { "psr-4": { - "Gt\\FileCache\\Test\\": "./test/phpunit" + "GT\\FileCache\\Test\\": "./test/phpunit" } }, diff --git a/composer.lock b/composer.lock index 1a769e7..8b85098 100644 --- a/composer.lock +++ b/composer.lock @@ -589,11 +589,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.50", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d452086fb4cf648c6b2d8cf3b639351f79e4f3e2", + "reference": "d452086fb4cf648c6b2d8cf3b639351f79e4f3e2", "shasum": "" }, "require": { @@ -638,7 +638,7 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-04-17T13:10:32+00:00" }, { "name": "phpunit/php-code-coverage", @@ -963,16 +963,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.62", + "version": "10.5.63", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3f7dd5066ebde5809296a81f0b19e8b00e5aab49" + "reference": "33198268dad71e926626b618f3ec3966661e4d90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3f7dd5066ebde5809296a81f0b19e8b00e5aab49", - "reference": "3f7dd5066ebde5809296a81f0b19e8b00e5aab49", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", "shasum": "" }, "require": { @@ -1044,7 +1044,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.62" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" }, "funding": [ { @@ -1068,7 +1068,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:32:38+00:00" + "time": "2026-01-27T05:48:37+00:00" }, { "name": "psr/container", @@ -2207,16 +2207,16 @@ }, { "name": "symfony/config", - "version": "v6.4.14", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef" + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/4e55e7e4ffddd343671ea972216d4509f46c22ef", - "reference": "4e55e7e4ffddd343671ea972216d4509f46c22ef", + "url": "https://api.github.com/repos/symfony/config/zipball/ce9cb0c0d281aaf188b802d4968e42bfb60701e9", + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9", "shasum": "" }, "require": { @@ -2262,7 +2262,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.14" + "source": "https://github.com/symfony/config/tree/v6.4.34" }, "funding": [ { @@ -2273,25 +2273,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-11-04T11:33:53+00:00" + "time": "2026-02-24T17:34:50+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.19", + "version": "v6.4.36", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "b343c3b2f1539fe41331657b37d5c96c1d1ea842" + "reference": "cd7881a6dc84b780411199cd0584e1a53a3b9ba7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/b343c3b2f1539fe41331657b37d5c96c1d1ea842", - "reference": "b343c3b2f1539fe41331657b37d5c96c1d1ea842", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/cd7881a6dc84b780411199cd0584e1a53a3b9ba7", + "reference": "cd7881a6dc84b780411199cd0584e1a53a3b9ba7", "shasum": "" }, "require": { @@ -2299,7 +2303,7 @@ "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", "symfony/service-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.2.10|^7.0" + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", @@ -2343,7 +2347,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.19" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.36" }, "funding": [ { @@ -2354,25 +2358,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-20T10:02:49+00:00" + "time": "2026-03-30T16:39:36+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -2385,7 +2393,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -2410,7 +2418,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -2426,20 +2434,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.13", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3" + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/4856c9cf585d5a0313d8d35afd681a526f038dd3", - "reference": "4856c9cf585d5a0313d8d35afd681a526f038dd3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3", + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3", "shasum": "" }, "require": { @@ -2476,7 +2484,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.13" + "source": "https://github.com/symfony/filesystem/tree/v6.4.34" }, "funding": [ { @@ -2487,25 +2495,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:07:50+00:00" + "time": "2026-02-24T17:51:06+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -2555,7 +2567,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" }, "funding": [ { @@ -2566,28 +2578,33 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -2635,7 +2652,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" }, "funding": [ { @@ -2646,25 +2663,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -2682,7 +2703,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -2718,7 +2739,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -2729,25 +2750,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/var-exporter", - "version": "v6.4.19", + "version": "v6.4.36", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "be6e71b0c257884c1107313de5d247741cfea172" + "reference": "f9c4a9695a9e2bbc65c920e147d8d7ae28f8d79a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/be6e71b0c257884c1107313de5d247741cfea172", - "reference": "be6e71b0c257884c1107313de5d247741cfea172", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/f9c4a9695a9e2bbc65c920e147d8d7ae28f8d79a", + "reference": "f9c4a9695a9e2bbc65c920e147d8d7ae28f8d79a", "shasum": "" }, "require": { @@ -2795,7 +2820,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v6.4.19" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.36" }, "funding": [ { @@ -2806,12 +2831,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-13T09:33:32+00:00" + "time": "2026-03-10T15:06:19+00:00" }, { "name": "theseer/tokenizer", diff --git a/example/01-latlon.php b/example/01-latlon.php index e71e16b..35c95c8 100644 --- a/example/01-latlon.php +++ b/example/01-latlon.php @@ -12,7 +12,7 @@ require __DIR__ . "/../vendor/autoload.php"; $startTime = microtime(true); -$fileCache = new Gt\FileCache\Cache("/tmp/ip-address-geolocation"); +$fileCache = new GT\FileCache\Cache("/tmp/ip-address-geolocation"); function httpJson(string $uri):object { $ch = curl_init($uri); @@ -22,14 +22,14 @@ function httpJson(string $uri):object { $response = curl_exec($ch); if($response === false) { - throw new Gt\FileCache\CacheValueGenerationException(curl_error($ch)); + 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( + throw new GT\FileCache\CacheValueGenerationException( "Invalid JSON returned from $uri", previous: $exception, ); diff --git a/example/02-class.php b/example/02-class.php index 847b9c7..66eb37d 100644 --- a/example/02-class.php +++ b/example/02-class.php @@ -1,7 +1,7 @@ dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_file($filePath)) { throw new FileNotFoundException($filePath); } @@ -18,7 +18,7 @@ public function getData(string $name):mixed { } public function setData(string $name, mixed $value):void { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_dir(dirname($filePath))) { mkdir(dirname($filePath), 0775, true); } @@ -26,7 +26,7 @@ public function setData(string $name, mixed $value):void { } public function checkValidity(string $name, int $secondsValidity):void { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_file($filePath)) { throw new CacheInvalidException("$filePath (does not exist)"); } @@ -37,11 +37,16 @@ public function checkValidity(string $name, int $secondsValidity):void { } public function invalidate(string $name):void { - $filePath = "$this->dirPath/$name"; + $filePath = $this->getFilePath($name); if(!is_file($filePath)) { return; } unlink($filePath); } + + private function getFilePath(string $name):string { + $escapedName = rawurlencode($name); + return "$this->dirPath/$escapedName"; + } } diff --git a/src/FileCacheException.php b/src/FileCacheException.php index 7ca90af..329c759 100644 --- a/src/FileCacheException.php +++ b/src/FileCacheException.php @@ -1,5 +1,5 @@ getSut(); + $name = "https://example.com/test"; + $value = "cached-value"; + + self::assertSame($value, $sut->get($name, fn() => $value)); + + $expectedFile = sys_get_temp_dir() + . "/phpgt-filecache/" + . rawurlencode($name); + self::assertFileExists($expectedFile); + self::assertSame($value, unserialize(file_get_contents($expectedFile))); + } + + public function testGet_urlName_doesNotTraverseFilesystem():void { + $sut = $this->getSut(); + $name = "../outside-cache"; + $value = "cached-value"; + + self::assertSame($value, $sut->get($name, fn() => $value)); + + $cacheDir = sys_get_temp_dir() . "/phpgt-filecache"; + $expectedFile = $cacheDir . "/" . rawurlencode($name); + self::assertFileExists($expectedFile); + self::assertFileDoesNotExist(sys_get_temp_dir() . "/outside-cache"); + } + public function testGet_generationExceptionDoesNotWriteInvalidValue():void { $fileAccess = self::createMock(FileAccess::class); $fileAccess->expects(self::once())