diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1e463..81b3ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Changed +- Added scalar, nullable, and `mixed` type declarations to the public cache API + and its configuration properties. Calls that already use the documented value + types remain compatible. +- PHPStan analysis now runs at level 8. - **BREAKING**: The `SimplePhpCache` global class is now `Tschueller\SimplePhpCache\SimplePhpCache`, loaded from `src/` through PSR-4. Manual includes must be replaced with Composer's autoloader. See the migration diff --git a/TODO.md b/TODO.md index 17c2b07..ad11245 100644 --- a/TODO.md +++ b/TODO.md @@ -2,22 +2,15 @@ Deferred improvements — implement when explicitly requested or when capacity allows. -## Medium Priority +## v1.x -- **[MEDIUM] Add proper return types and parameter types to all public methods** - Current code has no type declarations. Requires PHP 8.x syntax review. - Non-breaking as long as callers pass correct types. - Prerequisite for raising PHPStan level beyond 3. - -- **[MEDIUM] Raise PHPStan analysis level (target: 6–8)** - Currently level 3. Level 10 reveals 16 findings, all related to missing type declarations. - Can only be raised sustainably after type declarations are added. - -- **[MEDIUM] Implement PSR-16 SimpleCache interface** - Makes the library interoperable with PSR-16 consumers. Requires namespace + new - API surface. Large refactor — deserves its own PR. +- **[MEDIUM] Make cache writes atomic and protect readers from partial files** + `LOCK_EX` does not prevent readers from observing a file while it is being + written. Add an atomic-write strategy and concurrent-read/write tests. -## Low Priority +- **[MEDIUM] Publish to Packagist** + Run `composer validate --strict`, submit to packagist.org. + Prerequisite: stable tagged 1.0.0 release with namespace. - **[LOW] Add code coverage reporting to CI (Xdebug/PCOV)** Track test coverage over time. Optional quality gate. @@ -32,10 +25,8 @@ Deferred improvements — implement when explicitly requested or when capacity a Edge cases: empty string IDs, very long IDs, unreadable cache directory, Unicode in cache IDs. -- **[MEDIUM] Make cache writes atomic and protect readers from partial files** - `LOCK_EX` does not prevent readers from observing a file while it is being - written. Add an atomic-write strategy and concurrent-read/write tests. +## v2.x -- **[LOW] Publish to Packagist** - Run `composer validate --strict`, submit to packagist.org. - Prerequisite: stable tagged release with namespace. +- **[MEDIUM] Implement PSR-16 SimpleCache interface** + Adds a separate PSR-16 cache API and storage model. Deliberately out of scope + for the stable 1.x SimplePhpCache API; deserves its own major-version PR. diff --git a/phpstan.neon b/phpstan.neon index 2230803..72c366c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,6 @@ parameters: - level: 3 + level: 8 paths: - src + - tests ignoreErrors: [] diff --git a/src/SimplePhpCache.php b/src/SimplePhpCache.php index b79cadf..9d92cad 100644 --- a/src/SimplePhpCache.php +++ b/src/SimplePhpCache.php @@ -15,19 +15,19 @@ class SimplePhpCache private const VAR_CACHE_PREFIX = "SPCJSON1:"; /** Cache id from the current started cache. */ - private static $startedCache = null; + private static ?string $startedCache = null; /** The cached content. */ - private static $cacheContent = null; + private static mixed $cacheContent = null; /** Whether the current cache session contains a cached value. */ private static bool $hasCacheContent = false; /** The cache base directory. */ - public static $cacheBaseDir = null; + public static ?string $cacheBaseDir = null; /** The max cache time. */ - public static $maxCacheTime = 86400; + public static int $maxCacheTime = 86400; /** * Start the HTML output caching. @@ -40,7 +40,7 @@ class SimplePhpCache * @throws RuntimeException * When the cache is already started */ - public static function initHTMLCaching($id, $refresh = false) + public static function initHTMLCaching(string $id, bool $refresh = false): bool { if (self::$startedCache != null) { @@ -77,7 +77,7 @@ public static function initHTMLCaching($id, $refresh = false) * @throws RuntimeException * When the cache is not started */ - public static function finishHTMLCaching($id) + public static function finishHTMLCaching(string $id): string { if (self::$startedCache != $id) { @@ -86,12 +86,20 @@ public static function finishHTMLCaching($id) if (self::$hasCacheContent) { + if (!is_string(self::$cacheContent)) { + throw new RuntimeException("Invalid HTML cache content"); + } + $content = self::$cacheContent; } else { $cacheFile = self::getCacheDir() . "/" . self::getFilename($id); $content = ob_get_clean(); + if ($content === false) { + throw new RuntimeException("Error reading output buffer"); + } + if (file_put_contents($cacheFile, $content, LOCK_EX) === false) throw new RuntimeException("Error writing cache: '$cacheFile'"); } @@ -116,7 +124,7 @@ public static function finishHTMLCaching($id) * @throws RuntimeException * When the cache is already started */ - public static function initVarCaching($id, $refresh = false) + public static function initVarCaching(string $id, bool $refresh = false): bool { if (self::$startedCache != null) { @@ -163,7 +171,7 @@ public static function initVarCaching($id, $refresh = false) * The data to cache. * @throws RuntimeException */ - public static function setVarCaching($id, $data) + public static function setVarCaching(string $id, mixed $data): void { if (self::$startedCache != $id) { @@ -190,7 +198,7 @@ public static function setVarCaching($id, $data) * @throws RuntimeException * When the cache is not started */ - public static function finishVarCaching($id) + public static function finishVarCaching(string $id): mixed { if (self::$startedCache != $id) { @@ -214,7 +222,8 @@ public static function finishVarCaching($id) * @param string $idPrefix The cache identifier prefix. */ - public static function clearCache($id = null, $idPrefix = null) { + public static function clearCache(?string $id = null, ?string $idPrefix = null): void + { if ($id) { $pattern = self::getFilename($id); } else if ($idPrefix) { @@ -237,7 +246,8 @@ public static function clearCache($id = null, $idPrefix = null) { * @return int * The cache file count. */ - public static function getCacheCount($idPrefix = "") { + public static function getCacheCount(string $idPrefix = ""): int + { $pattern = self::sanitizeIdPrefix($idPrefix) . "*.cache"; return count(glob(self::getCacheDir() . "/" . $pattern) ?: []); } @@ -247,9 +257,9 @@ public static function getCacheCount($idPrefix = "") { * @param string $idPrefix * @return string */ - private static function sanitizeIdPrefix($idPrefix) + private static function sanitizeIdPrefix(string $idPrefix): string { - return preg_replace('/[^a-zA-Z0-9_\-.]/', '', (string) $idPrefix); + return preg_replace('/[^a-zA-Z0-9_\-.]/', '', $idPrefix) ?? ''; } /** @@ -260,7 +270,7 @@ private static function sanitizeIdPrefix($idPrefix) * @return string * The cache file name. */ - private static function getFilename($id) + private static function getFilename(string $id): string { return urlencode(self::fixPath($id)) . "-" . md5($id) . ".cache"; } @@ -271,7 +281,7 @@ private static function getFilename($id) * @param string $raw * @return array{0: bool, 1: mixed} Whether the payload is valid and its value. */ - private static function decodeVarCachePayload($raw) + private static function decodeVarCachePayload(string $raw): array { if (!str_starts_with($raw, self::VAR_CACHE_PREFIX)) { return [false, null]; @@ -293,7 +303,7 @@ private static function decodeVarCachePayload($raw) * @param mixed $data * @throws RuntimeException */ - private static function writeVarCachePayload($cacheFile, $data) + private static function writeVarCachePayload(string $cacheFile, mixed $data): void { try { $payload = self::VAR_CACHE_PREFIX . json_encode($data, JSON_THROW_ON_ERROR); @@ -312,7 +322,7 @@ private static function writeVarCachePayload($cacheFile, $data) * @param mixed $value * @return boolean */ - private static function containsObject($value) + private static function containsObject(mixed $value): bool { if (is_object($value)) { return true; @@ -337,7 +347,7 @@ private static function containsObject($value) * The dir path to fix. * @return string The fixed path. */ - private static function fixPath($path) + private static function fixPath(string $path): string { return str_replace("\\", "/", $path); } @@ -350,7 +360,7 @@ private static function fixPath($path) * @throws RuntimeException * When the cache directory creation failed. */ - private static function getCacheDir() + private static function getCacheDir(): string { if (self::$cacheBaseDir == null) { self::$cacheBaseDir = sys_get_temp_dir();