├── .github └── workflows │ └── test.yml ├── .gitignore ├── README.md ├── codeception.dist.yml ├── composer.json ├── src ├── FileCache.php └── InvalidArgumentException.php └── tests ├── TestableFileCache.php ├── _support ├── Helper │ └── Integration.php └── IntegrationTester.php ├── integration.suite.dist.yml └── integration ├── FileCacheCest.php └── FileCacheIntegrationTest.php /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Codeception tests 2 | 3 | on: [ push ] 4 | 5 | jobs: 6 | build: 7 | name: ${{matrix.operating-system}}, PHP ${{ matrix.php }} 8 | 9 | runs-on: ${{ matrix.operating-system }} 10 | 11 | strategy: 12 | matrix: 13 | operating-system: [ ubuntu-latest, ubuntu-20.04 ] 14 | php: [ '8.0', '8.1', '8.4' ] 15 | 16 | steps: 17 | - uses: actions/checkout@master 18 | 19 | - name: Setup PHP Action 20 | uses: shivammathur/setup-php@master 21 | with: 22 | php-version: ${{ matrix.php }} 23 | 24 | - name: Install dependencies 25 | run: composer install 26 | 27 | - name: Run tests 28 | run: composer run test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /composer.lock 3 | /vendor 4 | /tests/_output 5 | /tests/_support/_generated 6 | /tests/*.suite.yml 7 | /codeception.yml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | kodus/file-cache 2 | ================ 3 | 4 | [![PHP Version](https://img.shields.io/badge/php-8.0%2B-blue.svg)](https://packagist.org/packages/kodus/file-cache) 5 | [![Build Status](https://travis-ci.org/kodus/file-cache.svg?branch=master)](https://travis-ci.org/kodus/file-cache) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/kodus/file-cache/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/kodus/file-cache/?branch=master) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/kodus/file-cache/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/kodus/file-cache/?branch=master) 8 | 9 | This library provides a minimal [PSR-16](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-16-simple-cache.md) 10 | cache-implementation backed by simple file-system storage. 11 | 12 | This can be used to provide working, lightweight bootstrapping when you want to ship a project that works 13 | out of the box, but doesn't depend on an [awesome, full-blown caching-framework](http://www.scrapbook.cash/). 14 | 15 | 16 | ## Strategy 17 | 18 | Files are stored in a specified cache-folder, with two levels of sub-folders to avoid file-system limitations on 19 | the number of files per folder. (This will probably work okay for entry-numbers in the tens of thousands - if you're 20 | storing cache-entries in the millions, you should not be using a file-based cache.) 21 | 22 | To reduce storage overhead and speed up expiration time-checks, the file modification time will be set in the future. 23 | (The file creation timestamp will reflect the time the file was actually created.) 24 | 25 | ## Usage 26 | 27 | Please refer to the [PSR-16 spec](https://packagist.org/packages/psr/simple-cache) for the API description. 28 | 29 | ### Security 30 | 31 | In a production setting, consider specifying appropriate `$dir_mode` and `$file_mode` constructor-arguments for 32 | your hosting environment - the defaults are a typical choice, but you may be able to tighten permissions on your 33 | system, if needed. 34 | 35 | ### Garbage Collection 36 | 37 | Because this is a file-based cache, you do need to think about garbage-collection as it relates to your use-case. 38 | 39 | This cache-implementation does not do any automatic garbage-collection on-the-fly, because this would periodically 40 | block a user-request, and garbage-collection across a file-system isn't very fast. 41 | 42 | A public method `cleanExpired()` will flush expired entries - depending on your use-case, consider these options: 43 | 44 | 1. For cache-entries with non-dynamic keys (e.g. based on primary keys, URLs, etc. of user-managed 45 | data) you likely don't need garbage-collection. Manually clearing the folder once a year or so might suffice. 46 | 47 | 2. For cache-entries with dynamic keys (such as Session IDs, or other random or pseudo-random keys) you should 48 | set up a cron-job to call the `cleanExpired()` method periodically, say, once per day. 49 | 50 | For cache-entries with dynamic keys in the millions, as mentioned, you probably don't want a file-based cache. 51 | -------------------------------------------------------------------------------- /codeception.dist.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | coverage: 3 | enabled: true 4 | include: 5 | - src/* 6 | paths: 7 | tests: tests 8 | output: tests/_output 9 | data: tests/_data 10 | support: tests/_support 11 | envs: tests/_envs 12 | settings: 13 | colors: true 14 | memory_limit: 1024M 15 | extensions: 16 | enabled: 17 | - Codeception\Extension\RunFailed 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kodus/file-cache", 3 | "description": "Minimal PSR-16 cache-implementation", 4 | "authors": [ 5 | { 6 | "name": "Rasmus Schultz", 7 | "email": "rasc@jfmedier.dk", 8 | "role": "Developer" 9 | }, 10 | { 11 | "name": "Thomas Pedersen", 12 | "email": "thno@jfmedier.dk", 13 | "role": "Developer" 14 | } 15 | ], 16 | "license": "MIT", 17 | "minimum-stability": "dev", 18 | "prefer-stable": true, 19 | "provide": { 20 | "psr/simple-cache-implementation": "1.0" 21 | }, 22 | "require": { 23 | "php": ">= 8.0", 24 | "psr/simple-cache": "^2||^3" 25 | }, 26 | "require-dev": { 27 | "codeception/codeception": "^5", 28 | "cache/integration-tests": "dev-master", 29 | "codeception/module-asserts": "^3.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Kodus\\Cache\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Kodus\\Cache\\Test\\": "tests/", 39 | "Kodus\\Cache\\Test\\Integration\\": "tests/integration/" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "codecept run" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/FileCache.php: -------------------------------------------------------------------------------- 1 | default_ttl = $default_ttl; 57 | $this->dir_mode = $dir_mode; 58 | $this->file_mode = $file_mode; 59 | 60 | if (! file_exists($cache_path) && file_exists(dirname($cache_path))) { 61 | $this->mkdir($cache_path); // ensure that the parent path exists 62 | } 63 | 64 | $path = realpath($cache_path); 65 | 66 | if ($path === false) { 67 | throw new InvalidArgumentException("cache path does not exist: {$cache_path}"); 68 | } 69 | 70 | if (! is_writable($path . DIRECTORY_SEPARATOR)) { 71 | throw new InvalidArgumentException("cache path is not writable: {$cache_path}"); 72 | } 73 | 74 | $this->cache_path = $path; 75 | } 76 | 77 | public function get(string $key, mixed $default = null): mixed 78 | { 79 | $path = $this->getPath($key); 80 | 81 | $expires_at = @filemtime($path); 82 | 83 | if ($expires_at === false) { 84 | return $default; // file not found 85 | } 86 | 87 | if ($this->getTime() >= $expires_at) { 88 | @unlink($path); // file expired 89 | 90 | return $default; 91 | } 92 | 93 | $data = @file_get_contents($path); 94 | 95 | if ($data === false) { 96 | return $default; // race condition: file not found 97 | } 98 | 99 | if ($data === 'b:0;') { 100 | return false; // because we can't otherwise distinguish a FALSE return-value from unserialize() 101 | } 102 | 103 | $value = @unserialize($data); 104 | 105 | if ($value === false) { 106 | return $default; // unserialize() failed 107 | } 108 | 109 | return $value; 110 | } 111 | 112 | public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool 113 | { 114 | $path = $this->getPath($key); 115 | 116 | $dir = dirname($path); 117 | 118 | if (! file_exists($dir)) { 119 | // ensure that the parent path exists: 120 | $this->mkdir($dir); 121 | } 122 | 123 | $temp_path = $this->cache_path . DIRECTORY_SEPARATOR . uniqid('', true); 124 | 125 | if (is_int($ttl)) { 126 | $expires_at = $this->getTime() + $ttl; 127 | } elseif ($ttl instanceof DateInterval) { 128 | $expires_at = date_create_from_format("U", (string) $this->getTime())->add($ttl)->getTimestamp(); 129 | } elseif ($ttl === null) { 130 | $expires_at = $this->getTime() + $this->default_ttl; 131 | } else { 132 | throw new InvalidArgumentException("invalid TTL: " . print_r($ttl, true)); 133 | } 134 | 135 | if (false === @file_put_contents($temp_path, serialize($value))) { 136 | return false; 137 | } 138 | 139 | if (false === @chmod($temp_path, $this->file_mode)) { 140 | return false; 141 | } 142 | 143 | if (@touch($temp_path, $expires_at) && @rename($temp_path, $path)) { 144 | return true; 145 | } 146 | 147 | @unlink($temp_path); 148 | 149 | return false; 150 | } 151 | 152 | public function delete(string $key): bool 153 | { 154 | $this->validateKey($key); 155 | 156 | $path = $this->getPath($key); 157 | 158 | return !file_exists($path) || @unlink($path); 159 | } 160 | 161 | public function clear(): bool 162 | { 163 | $success = true; 164 | 165 | $paths = $this->listPaths(); 166 | 167 | foreach ($paths as $path) { 168 | if (! unlink($path)) { 169 | $success = false; 170 | } 171 | } 172 | 173 | return $success; 174 | } 175 | 176 | public function getMultiple(iterable $keys, mixed $default = null): iterable 177 | { 178 | $values = []; 179 | 180 | foreach ($keys as $key) { 181 | $values[$key] = $this->get($key) ?: $default; 182 | } 183 | 184 | return $values; 185 | } 186 | 187 | public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool 188 | { 189 | $ok = true; 190 | 191 | foreach ($values as $key => $value) { 192 | if (is_int($key)) { 193 | $key = (string) $key; 194 | } 195 | 196 | $this->validateKey($key); 197 | 198 | $ok = $this->set($key, $value, $ttl) && $ok; 199 | } 200 | 201 | return $ok; 202 | } 203 | 204 | public function deleteMultiple(iterable $keys): bool 205 | { 206 | $ok = true; 207 | 208 | foreach ($keys as $key) { 209 | $this->validateKey($key); 210 | 211 | $ok = $ok && $this->delete($key); 212 | } 213 | 214 | return $ok; 215 | } 216 | 217 | public function has(string $key): bool 218 | { 219 | return $this->get($key, $this) !== $this; 220 | } 221 | 222 | public function increment($key, $step = 1) 223 | { 224 | $path = $this->getPath($key); 225 | 226 | $dir = dirname($path); 227 | 228 | if (! file_exists($dir)) { 229 | $this->mkdir($dir); // ensure that the parent path exists 230 | } 231 | 232 | $lock_path = $dir . DIRECTORY_SEPARATOR . ".lock"; // allows max. 256 client locks at one time 233 | 234 | $lock_handle = fopen($lock_path, "w"); 235 | 236 | flock($lock_handle, LOCK_EX); 237 | 238 | $value = $this->get($key, 0) + $step; 239 | 240 | $ok = $this->set($key, $value); 241 | 242 | flock($lock_handle, LOCK_UN); 243 | 244 | return $ok ? $value : false; 245 | } 246 | 247 | public function decrement($key, $step = 1) 248 | { 249 | return $this->increment($key, -$step); 250 | } 251 | 252 | /** 253 | * Clean up expired cache-files. 254 | * 255 | * This method is outside the scope of the PSR-16 cache concept, and is specific to 256 | * this implementation, being a file-cache. 257 | * 258 | * In scenarios with dynamic keys (such as Session IDs) you should call this method 259 | * periodically - for example from a scheduled daily cron-job. 260 | * 261 | * @return void 262 | */ 263 | public function cleanExpired() 264 | { 265 | $now = $this->getTime(); 266 | 267 | $paths = $this->listPaths(); 268 | 269 | foreach ($paths as $path) { 270 | if ($now > filemtime($path)) { 271 | @unlink($path); 272 | } 273 | } 274 | } 275 | 276 | /** 277 | * For a given cache key, obtain the absolute file path 278 | * 279 | * @param string $key 280 | * 281 | * @return string absolute path to cache-file 282 | * 283 | * @throws InvalidArgumentException if the specified key contains a character reserved by PSR-16 284 | */ 285 | protected function getPath($key) 286 | { 287 | $this->validateKey($key); 288 | 289 | $hash = hash("sha256", $key); 290 | 291 | return $this->cache_path 292 | . DIRECTORY_SEPARATOR 293 | . strtoupper($hash[0]) 294 | . DIRECTORY_SEPARATOR 295 | . strtoupper($hash[1]) 296 | . DIRECTORY_SEPARATOR 297 | . substr($hash, 2); 298 | } 299 | 300 | /** 301 | * @return int current timestamp 302 | */ 303 | protected function getTime() 304 | { 305 | return time(); 306 | } 307 | 308 | /** 309 | * @return Generator|string[] 310 | */ 311 | protected function listPaths() 312 | { 313 | $iterator = new RecursiveDirectoryIterator( 314 | $this->cache_path, 315 | FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS 316 | ); 317 | 318 | $iterator = new RecursiveIteratorIterator($iterator); 319 | 320 | foreach ($iterator as $path) { 321 | if (is_dir($path)) { 322 | continue; // ignore directories 323 | } 324 | 325 | yield $path; 326 | } 327 | } 328 | 329 | /** 330 | * @param string $key 331 | * 332 | * @throws InvalidArgumentException 333 | */ 334 | protected function validateKey($key) 335 | { 336 | if (! is_string($key)) { 337 | $type = is_object($key) ? get_class($key) : gettype($key); 338 | 339 | throw new InvalidArgumentException("invalid key type: {$type} given"); 340 | } 341 | 342 | if ($key === "") { 343 | throw new InvalidArgumentException("invalid key: empty string given"); 344 | } 345 | 346 | if ($key === null) { 347 | throw new InvalidArgumentException("invalid key: null given"); 348 | } 349 | 350 | if (preg_match(self::PSR16_RESERVED, $key, $match) === 1) { 351 | throw new InvalidArgumentException("invalid character in key: {$match[0]}"); 352 | } 353 | } 354 | 355 | /** 356 | * Recursively create directories and apply permission mask 357 | * 358 | * @param string $path absolute directory path 359 | */ 360 | private function mkdir($path) 361 | { 362 | $parent_path = dirname($path); 363 | 364 | if (!file_exists($parent_path)) { 365 | $this->mkdir($parent_path); // recursively create parent dirs first 366 | } 367 | 368 | mkdir($path); 369 | chmod($path, $this->dir_mode); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | time_frozen = parent::getTime(); 22 | } 23 | 24 | protected function getTime() 25 | { 26 | return $this->time_frozen; 27 | } 28 | 29 | /** 30 | * @param int $seconds 31 | */ 32 | public function skipTime($seconds) 33 | { 34 | $this->time_frozen += $seconds; 35 | } 36 | 37 | /** 38 | * @param string $key 39 | * 40 | * @return string 41 | */ 42 | public function getCachePath($key) 43 | { 44 | return $this->getPath($key); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/_support/Helper/Integration.php: -------------------------------------------------------------------------------- 1 | cache = new TestableFileCache($path, self::DEFAULT_EXPIRATION, self::DIR_MODE, self::FILE_MODE); 34 | 35 | assert(file_exists($path)); 36 | 37 | assert(is_writable($path)); 38 | } 39 | 40 | public function _after() 41 | { 42 | $this->cache->clear(); 43 | } 44 | 45 | public function applyDirAndFilePermissions(IntegrationTester $I) 46 | { 47 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 48 | $I->comment("running on Windows - skipping test for *nix file permissions"); 49 | 50 | return; 51 | } 52 | 53 | $this->cache->set("key1", "value1"); 54 | 55 | $path = $this->cache->getCachePath("key1"); 56 | 57 | $dir_mode = fileperms(dirname($path)) & 0777; 58 | 59 | $I->assertSame(self::DIR_MODE, $dir_mode, sprintf("dir mode applied: %o", $dir_mode)); 60 | 61 | $file_mode = fileperms($path) & 0777; 62 | 63 | $I->assertSame(self::FILE_MODE, $file_mode, sprintf("file mode applied: %o", $file_mode)); 64 | } 65 | 66 | public function setGetAndDelete(IntegrationTester $I) 67 | { 68 | $I->assertTrue($this->cache->set("key1", "value1")); 69 | $I->assertTrue($this->cache->set("key2", "value2")); 70 | 71 | $I->assertSame("value1", $this->cache->get("key1")); 72 | $I->assertSame("value2", $this->cache->get("key2")); 73 | 74 | $I->assertTrue($this->cache->delete("key1"), "deleting existing value"); 75 | $I->assertTrue($this->cache->delete("key1"), "deleting non-existent value"); 76 | 77 | $I->assertSame(null, $this->cache->get("key1")); 78 | $I->assertSame("value2", $this->cache->get("key2")); 79 | 80 | $I->expectThrowable(InvalidArgumentException::class, function () { 81 | $this->cache->set("key@", "value1"); 82 | }); 83 | 84 | $I->expectThrowable(InvalidArgumentException::class, function () { 85 | $this->cache->get("key@"); 86 | }); 87 | 88 | $I->expectThrowable(InvalidArgumentException::class, function () { 89 | $this->cache->delete("key@"); 90 | }); 91 | } 92 | 93 | public function getNonExisting(IntegrationTester $I) 94 | { 95 | $I->assertSame(null, $this->cache->get("key")); 96 | $I->assertSame("default", $this->cache->get("key", "default")); 97 | } 98 | 99 | public function expirationInSeconds(IntegrationTester $I) 100 | { 101 | $this->cache->set("key", "value", 10); 102 | 103 | $this->cache->skipTime(5); 104 | 105 | $I->assertSame("value", $this->cache->get("key")); 106 | 107 | $this->cache->skipTime(5); 108 | 109 | $I->assertSame(null, $this->cache->get("key")); 110 | $I->assertSame("default", $this->cache->get("key", "default")); 111 | } 112 | 113 | public function expirationByInterval(IntegrationTester $I) 114 | { 115 | $interval = new DateInterval("PT10S"); 116 | 117 | $this->cache->set("key", "value", $interval); 118 | 119 | $this->cache->skipTime(5); 120 | 121 | $I->assertSame("value", $this->cache->get("key")); 122 | 123 | $this->cache->skipTime(5); 124 | 125 | $I->assertSame(null, $this->cache->get("key")); 126 | $I->assertSame("default", $this->cache->get("key", "default")); 127 | } 128 | 129 | public function expirationByDefault(IntegrationTester $I) 130 | { 131 | $this->cache->set("key", "value"); 132 | 133 | $this->cache->skipTime(self::DEFAULT_EXPIRATION - 5); 134 | 135 | $I->assertSame("value", $this->cache->get("key")); 136 | 137 | $this->cache->skipTime(10); 138 | 139 | $I->assertSame(null, $this->cache->get("key")); 140 | $I->assertSame("default", $this->cache->get("key", "default")); 141 | } 142 | 143 | public function expirationInThePast(IntegrationTester $I) 144 | { 145 | $this->cache->set("key1", "value1", 0); 146 | $this->cache->set("key2", "value2", -10); 147 | 148 | $I->assertSame("default", $this->cache->get("key1", "default")); 149 | $I->assertSame("default", $this->cache->get("key2", "default")); 150 | } 151 | 152 | public function clear(IntegrationTester $I) 153 | { 154 | // add some values that should be gone when we clear cache: 155 | 156 | $this->cache->set("key1", "value1"); 157 | $this->cache->set("key2", "value2"); 158 | 159 | $I->assertTrue($this->cache->clear()); 160 | 161 | // check to confirm everything"s been wiped out: 162 | 163 | $I->assertSame(null, $this->cache->get("key1")); 164 | $I->assertSame("default", $this->cache->get("key1", "default")); 165 | 166 | $I->assertSame(null, $this->cache->get("key2")); 167 | $I->assertSame("default", $this->cache->get("key2", "default")); 168 | } 169 | 170 | public function cleanExpired(IntegrationTester $I) 171 | { 172 | $this->cache->set("key1", "value1", 10); 173 | $this->cache->set("key2", "value2", 30); 174 | 175 | $this->cache->skipTime(20); 176 | 177 | $this->cache->cleanExpired(); 178 | 179 | $I->assertFileNotExists($this->cache->getCachePath("key1"), "file has expired"); 180 | $I->assertFileExists($this->cache->getCachePath("key2"), "file has not expired"); 181 | } 182 | 183 | public function testGetAndSetMultiple(IntegrationTester $I) 184 | { 185 | $this->cache->setMultiple(["key1" => "value1", "key2" => "value2"]); 186 | 187 | $results = $this->cache->getMultiple(["key1", "key2", "key3"], false); 188 | 189 | $I->assertSame(["key1" => "value1", "key2" => "value2", "key3" => false], $results); 190 | 191 | $I->expectThrowable(TypeError::class, function () { 192 | $this->cache->getMultiple("Invalid type"); 193 | }); 194 | 195 | $I->expectThrowable(TypeError::class, function () { 196 | $this->cache->setMultiple("Invalid type"); 197 | }); 198 | 199 | $I->expectThrowable(InvalidArgumentException::class, function () { 200 | $this->cache->setMultiple(["Invalid key@" => "value1"]); 201 | }); 202 | 203 | $I->expectThrowable(InvalidArgumentException::class, function () { 204 | $this->cache->getMultiple(["Invalid key@"]); 205 | }); 206 | } 207 | 208 | public function testDeleteMultiple(IntegrationTester $I) 209 | { 210 | $this->cache->setMultiple(["key1" => "value1", "key2" => "value2", "key3" => "value3"]); 211 | 212 | $this->cache->deleteMultiple(["key1", "key2"]); 213 | 214 | $I->assertSame(["key1" => null, "key2" => null], $this->cache->getMultiple(["key1", "key2"])); 215 | 216 | $I->assertSame("value3", $this->cache->get("key3")); 217 | 218 | $I->expectThrowable(TypeError::class, function () { 219 | $this->cache->deleteMultiple("Invalid type"); 220 | }); 221 | 222 | $I->expectThrowable(InvalidArgumentException::class, function () { 223 | $this->cache->deleteMultiple(["Invalid key@"]); 224 | }); 225 | } 226 | 227 | public function testHas(IntegrationTester $I) 228 | { 229 | $this->cache->set("key", "value"); 230 | 231 | $I->assertSame(true, $this->cache->has("key")); 232 | $I->assertSame(false, $this->cache->has("fudge")); 233 | } 234 | 235 | public function testIncrement(IntegrationTester $I) 236 | { 237 | // test setting initial value: 238 | 239 | $I->assertSame(5, $this->cache->increment("key", 5)); 240 | 241 | // test incrementing value: 242 | 243 | $I->assertSame(10, $this->cache->increment("key", 5)); 244 | $I->assertSame(11, $this->cache->increment("key")); 245 | } 246 | 247 | public function testDecrement(IntegrationTester $I) 248 | { 249 | // test setting initial value: 250 | 251 | $I->assertSame(10, $this->cache->increment("key", 10)); 252 | 253 | // test decrementing value: 254 | 255 | $I->assertSame(5, $this->cache->decrement("key", 5)); 256 | $I->assertSame(4, $this->cache->decrement("key")); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /tests/integration/FileCacheIntegrationTest.php: -------------------------------------------------------------------------------- 1 | 'New simple-cache v3 type hints break the test. FileCacheIntegrationTest contains updated versions.', 20 | 'testGetMultipleInvalidKeys' => 'New simple-cache v3 type hints break the test. FileCacheIntegrationTest contains updated versions.', 21 | 'testSetInvalidKeys' => 'New simple-cache v3 type hints break the test. FileCacheIntegrationTest contains updated versions.', 22 | 'testHasInvalidKeys' => 'New simple-cache v3 type hints break the test. FileCacheIntegrationTest contains updated versions.', 23 | 'testDeleteInvalidKeys' => 'New simple-cache v3 type hints break the test. FileCacheIntegrationTest contains updated versions.', 24 | ]; 25 | 26 | public function createSimpleCache() 27 | { 28 | $path = dirname(__DIR__) . "/_output/cache"; 29 | 30 | FileSystem::deleteDir($path); 31 | 32 | $cache = new FileCache($path, self::DEFAULT_EXPIRATION, self::DIR_MODE, self::FILE_MODE); 33 | 34 | assert(file_exists($path)); 35 | 36 | assert(is_writable($path)); 37 | 38 | return $cache; 39 | } 40 | 41 | /** 42 | * @dataProvider invalidStringKeys 43 | */ 44 | public function testGetInvalidStringKeys($key) 45 | { 46 | $this->expectException('Psr\SimpleCache\InvalidArgumentException'); 47 | $this->cache->get($key); 48 | } 49 | 50 | /** 51 | * @dataProvider invalidNonStringKeys 52 | */ 53 | public function testGetInvalidNonStringKeys($key) 54 | { 55 | $this->expectException(TypeError::class); 56 | $this->cache->get($key); 57 | } 58 | 59 | /** 60 | * @dataProvider invalidStringKeys 61 | */ 62 | public function testGetMultipleInvalidStringKeys($key) 63 | { 64 | $this->expectException('Psr\SimpleCache\InvalidArgumentException'); 65 | $this->cache->getMultiple(['key1', $key, 'key2']); 66 | } 67 | 68 | /** 69 | * @dataProvider invalidNonStringKeys 70 | */ 71 | public function testGetMultipleInvalidNonStringKeys($key) 72 | { 73 | $this->expectException(TypeError::class); 74 | $this->cache->getMultiple(['key1', $key, 'key2']); 75 | } 76 | 77 | public function testGetMultipleNoIterable() 78 | { 79 | $this->expectException(TypeError::class); 80 | $this->cache->getMultiple('key'); 81 | } 82 | 83 | /** 84 | * @dataProvider invalidStringKeys 85 | */ 86 | public function testSetInvalidStringKeys($key) 87 | { 88 | $this->expectException('Psr\SimpleCache\InvalidArgumentException'); 89 | $this->cache->set($key, 'foobar'); 90 | } 91 | 92 | /** 93 | * @dataProvider invalidNonStringKeys 94 | */ 95 | public function testSetInvalidNonStringKeys($key) 96 | { 97 | $this->expectException(TypeError::class); 98 | $this->cache->set($key, 'foobar'); 99 | } 100 | 101 | public function testSetMultipleNoIterable() 102 | { 103 | $this->expectException(TypeError::class); 104 | $this->cache->setMultiple('key'); 105 | } 106 | 107 | /** 108 | * @dataProvider invalidStringKeys 109 | */ 110 | public function testHasInvalidStringKeys($key) 111 | { 112 | $this->expectException('Psr\SimpleCache\InvalidArgumentException'); 113 | $this->cache->has($key); 114 | } 115 | /** 116 | * @dataProvider invalidNonStringKeys 117 | */ 118 | public function testHasInvalidNonStringKeys($key) 119 | { 120 | $this->expectException(TypeError::class); 121 | $this->cache->has($key); 122 | } 123 | 124 | /** 125 | * @dataProvider invalidStringKeys 126 | */ 127 | public function testDeleteInvalidStringKeys($key) 128 | { 129 | $this->expectException('Psr\SimpleCache\InvalidArgumentException'); 130 | $this->cache->delete($key); 131 | } 132 | 133 | /** 134 | * @dataProvider invalidNonStringKeys 135 | */ 136 | public function testDeleteInvalidNonStringKeys($key) 137 | { 138 | $this->expectException(TypeError::class); 139 | $this->cache->delete($key); 140 | } 141 | 142 | 143 | public function testDeleteMultipleNoIterable() 144 | { 145 | $this->expectException(TypeError::class); 146 | $this->cache->deleteMultiple('key'); 147 | } 148 | 149 | /** 150 | * @dataProvider invalidTtl 151 | */ 152 | public function testSetInvalidTtl($ttl) 153 | { 154 | $this->expectException(TypeError::class); 155 | $this->cache->set('key', 'value', $ttl); 156 | } 157 | 158 | /** 159 | * @dataProvider invalidTtl 160 | */ 161 | public function testSetMultipleInvalidTtl($ttl) 162 | { 163 | $this->expectException(TypeError::class); 164 | $this->cache->setMultiple(['key' => 'value'], $ttl); 165 | } 166 | 167 | public static function invalidNonStringKeys() 168 | { 169 | return [ 170 | [true], 171 | [false], 172 | [null], 173 | [2.5], 174 | [new \stdClass()], 175 | [['array']], 176 | ]; 177 | } 178 | 179 | public static function invalidStringKeys() 180 | { 181 | return [ 182 | [''], 183 | ['{str'], 184 | ['rand{'], 185 | ['rand{str'], 186 | ['rand}str'], 187 | ['rand(str'], 188 | ['rand)str'], 189 | ['rand/str'], 190 | ['rand\\str'], 191 | ['rand@str'], 192 | ['rand:str'], 193 | ]; 194 | } 195 | } 196 | --------------------------------------------------------------------------------