├── .github └── workflows │ └── php.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── Dockerfile ├── File.php ├── README.md ├── composer.json ├── modman └── tests ├── CommonBackend.php ├── CommonExtendedBackend.php └── FileBackendTest.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | services: 18 | redis: 19 | image: redis 20 | options: >- 21 | --health-cmd "redis-cli ping" 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | ports: 26 | - 6379:6379 27 | 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Validate composer.json and composer.lock 32 | run: composer validate --strict 33 | 34 | - name: Cache Composer packages 35 | id: composer-cache 36 | uses: actions/cache@v3 37 | with: 38 | path: vendor 39 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 40 | restore-keys: | 41 | ${{ runner.os }}-php- 42 | - name: Install dependencies 43 | run: composer install --prefer-dist --no-progress 44 | 45 | - name: Run PHPUnit test suite 46 | run: composer run-script test 47 | 48 | - name: Run PHP CS Fixer 49 | run: composer run-script php-cs-fixer -- --dry-run 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .basedir 2 | .idea 3 | vendor 4 | composer.lock 5 | .php-cs-fixer.cache 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRules([ 10 | '@PSR12' => true, 11 | ]) 12 | ->setFinder( 13 | PhpCsFixer\Finder::create() 14 | ->in([ 15 | './', 16 | 'tests/', 17 | ]) 18 | ->name('*.php') 19 | ->ignoreDotFiles(true) 20 | ->ignoreVCS(true) 21 | ); 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-alpine 2 | COPY --from=composer /usr/bin/composer /usr/bin/composer 3 | -------------------------------------------------------------------------------- /File.php: -------------------------------------------------------------------------------- 1 | null, // Path to cache files 43 | 'file_name_prefix' => 'cm', // Prefix for cache directories created 44 | 'file_locking' => true, // Best to keep enabled 45 | 'read_control' => false, // Use a checksum to detect corrupt data 46 | 'read_control_type' => 'crc32', // If read_control is enabled, which checksum algorithm to use 47 | 'hashed_directory_level' => 2, // How many characters should be used to create sub-directories 48 | 'use_chmod' => false, // Do not use chmod on files and directories (should use umask() to control permissions) 49 | 'directory_mode' => 0770, // Filesystem permissions for created directories (requires use_chmod) 50 | 'file_mode' => 0660, // Filesystem permissions for created files (requires use_chmod) 51 | ); 52 | 53 | /** @var bool */ 54 | protected $_isTagDirChecked; 55 | 56 | /** 57 | * @param array $options 58 | */ 59 | public function __construct(array $options = array()) 60 | { 61 | // Magento-friendly cache dir 62 | if (empty($options['cache_dir']) && class_exists('Mage', false)) { 63 | $options['cache_dir'] = Mage::getBaseDir('cache'); 64 | } 65 | 66 | // Backwards compatibility ZF 1.11 and ZF 1.12 67 | if (isset($options['hashed_directory_umask'])) { 68 | $options['directory_mode'] = $options['hashed_directory_umask']; 69 | } 70 | if (isset($options['cache_file_umask'])) { 71 | $options['file_mode'] = $options['cache_file_umask']; 72 | } 73 | 74 | // Auto-enable chmod if modes are specified. 75 | if (isset($options['directory_mode']) || isset($options['file_mode'])) { 76 | $options['use_chmod'] = true; 77 | } 78 | 79 | // Don't use parent constructor 80 | foreach ($options as $name => $value) { 81 | $this->setOption($name, $value); 82 | } 83 | 84 | // Check cache dir 85 | if ($this->_options['cache_dir'] !== null) { // particular case for this option 86 | $this->setCacheDir($this->_options['cache_dir']); 87 | } else { 88 | $this->setCacheDir(self::getTmpDir() . DIRECTORY_SEPARATOR, false); 89 | } 90 | 91 | // Validate prefix 92 | if (isset($this->_options['file_name_prefix']) && !preg_match('~^[a-zA-Z0-9_]+$~D', $this->_options['file_name_prefix'])) { 93 | Zend_Cache::throwException('Invalid file_name_prefix : must use only [a-zA-Z0-9_]'); 94 | } 95 | 96 | // See #ZF-4422 97 | if (is_string($this->_options['directory_mode'])) { 98 | $this->_options['directory_mode'] = octdec($this->_options['directory_mode']); 99 | } 100 | if (is_string($this->_options['file_mode'])) { 101 | $this->_options['file_mode'] = octdec($this->_options['file_mode']); 102 | } 103 | $this->_options['hashed_directory_umask'] = $this->_options['directory_mode']; 104 | $this->_options['cache_file_umask'] = $this->_options['file_mode']; 105 | } 106 | 107 | /** 108 | * OVERRIDDEN to remove use of each() which is deprecated in PHP 7.2 109 | * 110 | * Set the frontend directives 111 | * 112 | * @param array $directives Assoc of directives 113 | * @throws Zend_Cache_Exception 114 | * @return void 115 | */ 116 | public function setDirectives($directives) 117 | { 118 | if (!is_array($directives)) { 119 | Zend_Cache::throwException('Directives parameter must be an array'); 120 | } 121 | foreach ($directives as $name => $value) { 122 | if (!is_string($name)) { 123 | Zend_Cache::throwException("Incorrect option name : $name"); 124 | } 125 | $name = strtolower($name); 126 | if (array_key_exists($name, $this->_directives)) { 127 | $this->_directives[$name] = $value; 128 | } 129 | 130 | } 131 | 132 | $this->_loggerSanity(); 133 | } 134 | 135 | /** 136 | * Test if a cache is available for the given id and (if yes) return it (false else) 137 | * 138 | * @param string $id cache id 139 | * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested 140 | * @return string|bool cached datas 141 | */ 142 | public function load($id, $doNotTestCacheValidity = false) 143 | { 144 | $file = $this->_file($id); 145 | $cache = $this->_getCache($file, true); 146 | if (! $cache) { 147 | return false; 148 | } 149 | list($metadatas, $data) = $cache; 150 | if (! $doNotTestCacheValidity && (time() > $metadatas['expire'])) { 151 | // ?? $this->remove($id); 152 | return false; 153 | } 154 | if ($this->_options['read_control']) { 155 | $hashData = $this->_hash($data, $this->_options['read_control_type']); 156 | $hashControl = $metadatas['hash']; 157 | if ($hashData != $hashControl) { 158 | // Problem detected by the read control ! 159 | $this->_log('Zend_Cache_Backend_File::load() / read_control : stored hash and computed hash do not match'); 160 | $this->remove($id); 161 | return false; 162 | } 163 | } 164 | return $data; 165 | } 166 | 167 | /** 168 | * Save some string datas into a cache record 169 | * 170 | * Note : $data is always "string" (serialization is done by the 171 | * core not by the backend) 172 | * 173 | * @param string $data Datas to cache 174 | * @param string $id Cache id 175 | * @param array $tags Array of strings, the cache record will be tagged by each string entry 176 | * @param bool|int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) 177 | * @return boolean true if no problem 178 | */ 179 | public function save($data, $id, $tags = array(), $specificLifetime = false) 180 | { 181 | $file = $this->_file($id); 182 | $path = $this->_path($id); 183 | if ($this->_options['hashed_directory_level'] > 0) { 184 | if (!is_writable($path)) { 185 | // maybe, we just have to build the directory structure 186 | $this->_recursiveMkdirAndChmod($id); 187 | } 188 | if (!is_writable($path)) { 189 | return false; 190 | } 191 | } 192 | if ($this->_options['read_control']) { 193 | $hash = $this->_hash($data, $this->_options['read_control_type']); 194 | } else { 195 | $hash = ''; 196 | } 197 | $metadatas = array( 198 | 'hash' => $hash, 199 | 'mtime' => time(), 200 | 'expire' => $this->_expireTime($this->getLifetime($specificLifetime)), 201 | 'tags' => implode(',', $tags), 202 | ); 203 | $res = $this->_filePutContents($file, serialize($metadatas)."\n".$data); 204 | $res = $res && $this->_updateIdsTags(array($id), $tags, 'merge'); 205 | return $res; 206 | } 207 | 208 | /** 209 | * Remove a cache record 210 | * 211 | * @param string $id cache id 212 | * @return boolean true if no problem 213 | */ 214 | public function remove($id) 215 | { 216 | $file = $this->_file($id); 217 | $metadatas = $this->_getCache($file, false); 218 | if ($metadatas) { 219 | $boolRemove = $this->_remove($file); 220 | $boolTags = $this->_updateIdsTags(array($id), explode(',', $metadatas['tags']), 'diff'); 221 | return $boolRemove && $boolTags; 222 | } 223 | return false; 224 | } 225 | 226 | /** 227 | * Clean some cache records 228 | * 229 | * Available modes are : 230 | * 'all' (default) => remove all cache entries ($tags is not used) 231 | * 'old' => remove too old cache entries ($tags is not used) 232 | * 'matchingTag' => remove cache entries matching all given tags 233 | * ($tags can be an array of strings or a single string) 234 | * 'notMatchingTag' => remove cache entries not matching one of the given tags 235 | * ($tags can be an array of strings or a single string) 236 | * 'matchingAnyTag' => remove cache entries matching any given tags 237 | * ($tags can be an array of strings or a single string) 238 | * 239 | * @param string $mode 240 | * @param array $tags 241 | * @return boolean true if no problem 242 | */ 243 | public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 244 | { 245 | // We use this protected method to hide the recursive stuff 246 | clearstatcache(); 247 | switch($mode) { 248 | case Zend_Cache::CLEANING_MODE_ALL: 249 | case Zend_Cache::CLEANING_MODE_OLD: 250 | return $this->_clean($this->_options['cache_dir'], $mode); 251 | default: 252 | return $this->_cleanNew($mode, $tags); 253 | } 254 | } 255 | 256 | /** 257 | * Return an array of stored tags 258 | * 259 | * @return array array of stored tags (string) 260 | */ 261 | public function getTags() 262 | { 263 | $prefix = $this->_tagFile(''); 264 | $prefixLen = strlen($prefix); 265 | $tags = array(); 266 | foreach (@glob($prefix . '*') as $tagFile) { 267 | $tags[] = substr($tagFile, $prefixLen); 268 | } 269 | return $tags; 270 | } 271 | 272 | /** 273 | * Return an array of stored cache ids which match given tags 274 | * 275 | * In case of multiple tags, a logical AND is made between tags 276 | * 277 | * @param array $tags array of tags 278 | * @return array array of matching cache ids (string) 279 | */ 280 | public function getIdsMatchingTags($tags = array()) 281 | { 282 | return $this->_getIdsByTags(Zend_Cache::CLEANING_MODE_MATCHING_TAG, $tags, false); 283 | } 284 | 285 | /** 286 | * Return an array of stored cache ids which don't match given tags 287 | * 288 | * In case of multiple tags, a logical OR is made between tags 289 | * 290 | * @param array $tags array of tags 291 | * @return array array of not matching cache ids (string) 292 | */ 293 | public function getIdsNotMatchingTags($tags = array()) 294 | { 295 | return $this->_getIdsByTags(Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG, $tags, false); 296 | } 297 | 298 | /** 299 | * Return an array of stored cache ids which match any given tags 300 | * 301 | * In case of multiple tags, a logical AND is made between tags 302 | * 303 | * @param array $tags array of tags 304 | * @return array array of any matching cache ids (string) 305 | */ 306 | public function getIdsMatchingAnyTags($tags = array()) 307 | { 308 | return $this->_getIdsByTags(Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG, $tags, false); 309 | } 310 | 311 | /** 312 | * Return an array of metadatas for the given cache id 313 | * 314 | * The array must include these keys : 315 | * - expire : the expire timestamp 316 | * - tags : a string array of tags 317 | * - mtime : timestamp of last modification time 318 | * 319 | * @param string $id cache id 320 | * @return array array of metadatas (false if the cache id is not found) 321 | */ 322 | public function getMetadatas($id) 323 | { 324 | $metadatas = $this->_getCache($this->_file($id), false); 325 | if ($metadatas) { 326 | $metadatas['tags'] = explode(',', $metadatas['tags']); 327 | } 328 | return $metadatas; 329 | } 330 | 331 | /** 332 | * Give (if possible) an extra lifetime to the given cache id 333 | * 334 | * @param string $id cache id 335 | * @param int $extraLifetime 336 | * @return boolean true if ok 337 | */ 338 | public function touch($id, $extraLifetime) 339 | { 340 | $file = $this->_file($id); 341 | $cache = $this->_getCache($file, true); 342 | if (!$cache) { 343 | return false; 344 | } 345 | list($metadatas, $data) = $cache; 346 | if (time() > $metadatas['expire']) { 347 | return false; 348 | } 349 | $newMetadatas = array( 350 | 'hash' => $metadatas['hash'], 351 | 'mtime' => time(), 352 | 'expire' => $metadatas['expire'] + $extraLifetime, 353 | 'tags' => $metadatas['tags'] 354 | ); 355 | return !! $this->_filePutContents($file, serialize($newMetadatas)."\n".$data); 356 | } 357 | 358 | /** 359 | * Get a metadatas record and optionally the data as well 360 | * 361 | * @param string $file Cache file 362 | * @param bool $withData 363 | * @return array|bool 364 | */ 365 | protected function _getCache($file, $withData) 366 | { 367 | if (!is_file($file) || ! ($fd = @fopen($file, 'rb'))) { 368 | return false; 369 | } 370 | if ($this->_options['file_locking']) { 371 | flock($fd, LOCK_SH); 372 | } 373 | $metadata = fgets($fd); 374 | if (! $metadata) { 375 | if ($this->_options['file_locking']) { 376 | flock($fd, LOCK_UN); 377 | } 378 | fclose($fd); 379 | return false; 380 | } 381 | if ($withData) { 382 | $data = stream_get_contents($fd); 383 | } 384 | if ($this->_options['file_locking']) { 385 | flock($fd, LOCK_UN); 386 | } 387 | fclose($fd); 388 | $metadata = @unserialize(rtrim($metadata, "\n"), ['allowed_classes' => false]); 389 | if ($metadata === false) { 390 | return false; 391 | } 392 | if ($withData) { 393 | return array($metadata, $data); 394 | } 395 | return $metadata; 396 | } 397 | 398 | /** 399 | * Get meta data from a cache record 400 | * 401 | * @param string $id Cache id 402 | * @return array|bool Associative array of meta data 403 | */ 404 | protected function _getMetadatas($id) 405 | { 406 | return $this->_getCache($this->_file($id), false); 407 | } 408 | 409 | /** 410 | * Set a metadatas record 411 | * 412 | * @param string $id Cache id 413 | * @param array $metadatas Associative array of metadatas 414 | * @param boolean $save optional pass false to disable saving to file 415 | * @return boolean True if no problem 416 | */ 417 | protected function _setMetadatas($id, $metadatas, $save = true) 418 | { 419 | // TODO - implement for unit tests ___expire method 420 | return true; 421 | } 422 | 423 | /** 424 | * Return the complete directory path of a filename (including hashedDirectoryStructure) 425 | * 426 | * Uses multiple letters for a single-level hash rather than multiple levels 427 | * 428 | * @param string $id Cache id 429 | * @param boolean $parts if true, returns array of directory parts instead of single string 430 | * @return string|array Complete directory path 431 | */ 432 | protected function _path($id, $parts = false) 433 | { 434 | $partsArray = array(); 435 | $root = $this->_options['cache_dir']; 436 | $prefix = $this->_options['file_name_prefix']; 437 | if ($this->_options['hashed_directory_level'] > 0) { 438 | $root .= $prefix . '--' . substr(md5($id), -$this->_options['hashed_directory_level']) . DIRECTORY_SEPARATOR; 439 | $partsArray[] = $root; 440 | } 441 | if ($parts) { 442 | return $partsArray; 443 | } 444 | return $root; 445 | 446 | } 447 | 448 | /** 449 | * Clean some cache records (protected method used for recursive stuff) 450 | * 451 | * Available modes are : 452 | * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 453 | * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 454 | * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 455 | * ($tags can be an array of strings or a single string) 456 | * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 457 | * ($tags can be an array of strings or a single string) 458 | * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 459 | * ($tags can be an array of strings or a single string) 460 | * 461 | * @param string $dir Directory to clean 462 | * @param string $mode Clean mode 463 | * @param array $tags 464 | * @throws Zend_Cache_Exception 465 | * @return boolean True if no problem 466 | */ 467 | protected function _clean($dir, $mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 468 | { 469 | if (!is_dir($dir)) { 470 | return false; 471 | } 472 | if ($mode == 'all' && $dir === $this->_options['cache_dir']) { 473 | $glob = glob($this->_tagFile('*')); 474 | if ($glob !== false) { 475 | foreach ($glob as $tagFile) { 476 | @unlink($tagFile); 477 | } 478 | } 479 | } 480 | $result = true; 481 | $glob = @glob($dir . $this->_options['file_name_prefix'] . '--*'); 482 | if ($glob === false) { 483 | return true; 484 | } 485 | foreach ($glob as $file) { 486 | if (is_file($file)) { 487 | if ($mode == Zend_Cache::CLEANING_MODE_ALL) { 488 | $result = @unlink($file) && $result; 489 | continue; 490 | } 491 | 492 | $id = $this->_fileNameToId(basename($file)); 493 | $_file = $this->_file($id); 494 | if ($file != $_file) { 495 | @unlink($file); 496 | continue; 497 | } 498 | $metadatas = $this->_getCache($file, false); 499 | if (! $metadatas) { 500 | @unlink($file); 501 | continue; 502 | } 503 | if ($mode == Zend_Cache::CLEANING_MODE_OLD) { 504 | if (time() > $metadatas['expire']) { 505 | $result = $this->_remove($file) && $result; 506 | $result = $this->_updateIdsTags(array($id), explode(',', $metadatas['tags']), 'diff') && $result; 507 | } 508 | continue; 509 | } else { 510 | Zend_Cache::throwException('Invalid mode for clean() method.'); 511 | } 512 | } 513 | if (is_dir($file) && $this->_options['hashed_directory_level'] > 0) { 514 | // Recursive call 515 | $result = $this->_clean($file . DIRECTORY_SEPARATOR, $mode) && $result; 516 | if ($mode == 'all') { 517 | // if mode=='all', we try to drop the structure too 518 | @rmdir($file); 519 | } 520 | } 521 | } 522 | return $result; 523 | } 524 | 525 | /** 526 | * Clean some cache records (protected method used for recursive stuff) 527 | * 528 | * Available modes are : 529 | * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) 530 | * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) 531 | * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags 532 | * ($tags can be an array of strings or a single string) 533 | * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} 534 | * ($tags can be an array of strings or a single string) 535 | * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags 536 | * ($tags can be an array of strings or a single string) 537 | * 538 | * @param string $mode Clean mode 539 | * @param array $tags Array of tags 540 | * @throws Zend_Cache_Exception 541 | * @return boolean True if no problem 542 | */ 543 | protected function _cleanNew($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) 544 | { 545 | $result = true; 546 | $ids = $this->_getIdsByTags($mode, $tags, true); 547 | switch($mode) { 548 | case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: 549 | case Zend_Cache::CLEANING_MODE_MATCHING_TAG: 550 | $this->_updateIdsTags($ids, $tags, 'diff'); 551 | break; 552 | } 553 | foreach ($ids as $id) { 554 | $idFile = $this->_file($id); 555 | if (is_file($idFile)) { 556 | $result = $this->_remove($idFile) && $result; 557 | } 558 | } 559 | return $result; 560 | } 561 | 562 | /** 563 | * @param string $mode 564 | * @param array $tags 565 | * @param boolean $delete 566 | * @return array 567 | */ 568 | protected function _getIdsByTags($mode, $tags, $delete) 569 | { 570 | $ids = array(); 571 | switch($mode) { 572 | case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: 573 | $ids = $this->getIds(); 574 | if ($tags) { 575 | foreach ($tags as $tag) { 576 | if (! $ids) { 577 | break; // early termination optimization 578 | } 579 | $ids = array_diff($ids, $this->_getTagIds($tag)); 580 | } 581 | } 582 | break; 583 | case Zend_Cache::CLEANING_MODE_MATCHING_TAG: 584 | if ($tags) { 585 | $tag = array_shift($tags); 586 | $ids = $this->_getTagIds($tag); 587 | foreach ($tags as $tag) { 588 | if (! $ids) { 589 | break; // early termination optimization 590 | } 591 | $ids = array_intersect($ids, $this->_getTagIds($tag)); 592 | } 593 | $ids = array_unique($ids); 594 | } 595 | break; 596 | case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: 597 | foreach ($tags as $tag) { 598 | $file = $this->_tagFile($tag); 599 | if (! is_file($file) || ! ($fd = @fopen($file, 'rb+'))) { 600 | continue; 601 | } 602 | if ($this->_options['file_locking']) { 603 | flock($fd, LOCK_EX); 604 | } 605 | $ids = array_merge($ids, $this->_getTagIds($fd)); 606 | if ($delete) { 607 | fseek($fd, 0); 608 | ftruncate($fd, 0); 609 | } 610 | if ($this->_options['file_locking']) { 611 | flock($fd, LOCK_UN); 612 | } 613 | fclose($fd); 614 | } 615 | $ids = array_unique($ids); 616 | break; 617 | } 618 | return $ids; 619 | } 620 | 621 | /** 622 | * Make and return a file name (with path) 623 | * 624 | * @param string $id Cache id 625 | * @return string File name (with path) 626 | */ 627 | protected function _tagFile($id) 628 | { 629 | $path = $this->_tagPath(); 630 | $fileName = $this->_idToFileName($id); 631 | return $path . $fileName; 632 | } 633 | 634 | /** 635 | * Return the complete directory path where tags are stored 636 | * 637 | * @return string Complete directory path 638 | */ 639 | protected function _tagPath() 640 | { 641 | $path = $this->_options['cache_dir'] . DIRECTORY_SEPARATOR . $this->_options['file_name_prefix']. '-tags' . DIRECTORY_SEPARATOR; 642 | if (! $this->_isTagDirChecked) { 643 | if (! is_dir($path)) { 644 | if (@mkdir($path, $this->_options['use_chmod'] ? $this->_options['directory_mode'] : 0777) && $this->_options['use_chmod']) { 645 | @chmod($path, $this->_options['directory_mode']); // see #ZF-320 (this line is required in some configurations) 646 | } 647 | } 648 | $this->_isTagDirChecked = true; 649 | } 650 | return $path; 651 | } 652 | 653 | /** 654 | * @param string|resource $tag 655 | * @return array 656 | */ 657 | protected function _getTagIds($tag) 658 | { 659 | if (is_resource($tag)) { 660 | $ids = stream_get_contents($tag); 661 | } elseif(file_exists($this->_tagFile($tag))) { 662 | $ids = @file_get_contents($this->_tagFile($tag)); 663 | } else { 664 | $ids = false; 665 | } 666 | if(! $ids) { 667 | return array(); 668 | } 669 | $ids = trim(substr($ids, 0, strrpos($ids, "\n"))); 670 | return $ids ? explode("\n", $ids) : array(); 671 | } 672 | 673 | /** 674 | * @param array $ids 675 | * @param array $tags 676 | * @param string $mode 677 | * @return bool 678 | */ 679 | protected function _updateIdsTags($ids, $tags, $mode) 680 | { 681 | $result = true; 682 | if (empty($ids)) { 683 | return $result; 684 | } 685 | foreach($tags as $tag) { 686 | $file = $this->_tagFile($tag); 687 | if (file_exists($file)) { 688 | if ($mode == 'diff' || (mt_rand(1, 100) == 1 && filesize($file) > 4096)) { 689 | $file = $this->_tagFile($tag); 690 | if (! ($fd = @fopen($file, 'rb+'))) { 691 | $result = false; 692 | continue; 693 | } 694 | if ($this->_options['file_locking']) { 695 | flock($fd, LOCK_EX); 696 | } 697 | if ($mode == 'diff') { 698 | $_ids = array_diff($this->_getTagIds($fd), $ids); 699 | } else { 700 | $_ids = array_merge($this->_getTagIds($fd), $ids); 701 | } 702 | fseek($fd, 0); 703 | ftruncate($fd, 0); 704 | $result = fwrite($fd, implode("\n", array_unique($_ids))."\n") && $result; 705 | if ($this->_options['file_locking']) { 706 | flock($fd, LOCK_UN); 707 | } 708 | fclose($fd); 709 | } else { 710 | $result = file_put_contents($file, implode("\n", $ids)."\n", FILE_APPEND | ($this->_options['file_locking'] ? LOCK_EX : 0)) && $result; 711 | } 712 | } elseif ($mode == 'merge') { 713 | $result = $this->_filePutContents($file, implode("\n", $ids)."\n") && $result; 714 | } 715 | } 716 | return $result; 717 | } 718 | 719 | /** 720 | * Put the given string into the given file 721 | * 722 | * @param string $file File complete path 723 | * @param string $string String to put in file 724 | * @return boolean true if no problem 725 | */ 726 | protected function _filePutContents($file, $string) 727 | { 728 | $result = @file_put_contents($file, $string, $this->_options['file_locking'] ? LOCK_EX : 0); 729 | if ($result && $this->_options['use_chmod']) { 730 | @chmod($file, $this->_options['file_mode']); 731 | } 732 | return $result; 733 | } 734 | 735 | /** 736 | * Make the directory structure for the given id 737 | * 738 | * @param string $id cache id 739 | * @return boolean true 740 | */ 741 | protected function _recursiveMkdirAndChmod($id) 742 | { 743 | if ($this->_options['hashed_directory_level'] <= 0) { 744 | return true; 745 | } 746 | $partsArray = $this->_path($id, true); 747 | foreach ($partsArray as $part) { 748 | if (!is_dir($part)) { 749 | @mkdir($part, $this->_options['use_chmod'] ? $this->_options['directory_mode'] : 0777); 750 | if ($this->_options['use_chmod']) { 751 | @chmod($part, $this->_options['directory_mode']); // see #ZF-320 (this line is required in some configurations) 752 | } 753 | } 754 | } 755 | return true; 756 | } 757 | 758 | /** 759 | * For unit testing only 760 | * @param $id 761 | */ 762 | public function ___expire($id) 763 | { 764 | $metadata = $this->_getMetadatas($id); 765 | $this->touch($id, 1 - $metadata['expire']); 766 | } 767 | 768 | } 769 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cm_Cache_Backend_File 2 | ===================== 3 | 4 | The stock `Zend_Cache_Backend_File` backend has extremely poor performance for 5 | cleaning by tags making it become unusable as the number of cached items 6 | increases. This backend makes many changes resulting in a huge performance boost, 7 | especially for tag cleaning. 8 | 9 | This cache backend works by indexing tags in files so that tag operations 10 | do not require a full scan of every cache file. The ids are written to the 11 | tag files in append-only mode and only when files exceed 4k and only randomly 12 | are the tag files compacted to prevent endless growth in edge cases. 13 | 14 | The metadata and the cache record are stored in the same file rather than separate 15 | files resulting in fewer inodes and fewer file stat/read/write/lock/unlink operations. 16 | Also, the original hashed directory structure had very poor distribution due to 17 | the adler32 hashing algorithm and prefixes. The multi-level nested directories 18 | have been dropped in favor of single-level nesting made from multiple characters. 19 | 20 | Is the improvement substantial? Definitely. Tag cleaning is literally thousands of 21 | times faster, loading is twice as fast, and saving is slightly slower dependent on 22 | the number of tags being saved. 23 | 24 | Test it for yourself with the [Magento Cache Benchmark](https://github.com/colinmollenhour/magento-cache-benchmark). 25 | 26 | Installation 27 | ------------ 28 | 29 | 1. Install with Composer: `composer require colinmollenhour/cache-backend-file` 30 | 2. Edit `app/etc/local.xml` changing `global/cache/backend` to `Cm_Cache_Backend_File` (Magento 1 / OpenMage) 31 | 3. Delete all contents of the cache directory 32 | 33 | Example Configuration 34 | --------------------- 35 | 36 | ```xml 37 | 38 | 39 | 40 | Cm_Cache_Backend_File 41 | 42 | ... 43 | 44 | ... 45 | 46 | ``` 47 | 48 | By default, `Cm_Cache_Backend_File` is configured *not* to use chmod to set file permissions. The 49 | proper way to do file permissions is to respect the umask and not set any permissions. This way 50 | the file permissions can be properly inherited using the OS conventions. To improve security the 51 | umask should be properly set. In Magento the umask is set in `index.php` as 0 which means no 52 | restrictions. So, for example to make files and directories no longer public add `umask(0007)` to 53 | `Mage.php`. 54 | 55 | If umasks are too complicated and you prefer the sub-optimal (less-secure, needless system calls) 56 | approach you can enable the legacy chmod usage as seen below. This will force the file modes to be 57 | set regardless of the umask. 58 | 59 | ```xml 60 | 61 | 62 | 63 | Cm_Cache_Backend_File 64 | 65 | 1 66 | 0777 67 | 0666 68 | 69 | 70 | ... 71 | 72 | ... 73 | 74 | ``` 75 | 76 | For `directory_mode` the setgid bit can be set using 2 for the forth digit. E.g. 02770. This 77 | will cause files and directories created within the directory with the setgid bit to inherit the 78 | same group as the parent which is useful if you run scripts as users other than your web server user. 79 | The setgid bit can also be used with the default configuration (use_chmod off) by simply setting 80 | the bit on the var/cache directory one time using `chmod g+s var/cache`. 81 | 82 | Note that running your cron job as root is not a good practice from a security standpoint. 83 | 84 | Cleaning Old Files 85 | ------------------ 86 | 87 | Magento and Zend_Cache do not cleanup old records by themselves so if you want to 88 | keep your cache directory tidy you need to write and invoke regularly your own script 89 | which cleans the old data. Here is an example for Magento: 90 | 91 | ```php 92 | :P'); 93 | ini_set('memory_limit','1024M'); 94 | set_time_limit(0); 95 | error_reporting(E_ALL | E_STRICT); 96 | require_once 'app/Mage.php'; 97 | Mage::app()->getCache()->getBackend()->clean(Zend_Cache::CLEANING_MODE_OLD); 98 | // uncomment this for Magento Enterprise Edition 99 | // Enterprise_PageCache_Model_Cache::getCacheInstance()->getFrontend()->getBackend()->clean(Zend_Cache::CLEANING_MODE_OLD); 100 | ``` 101 | 102 | Development 103 | ----------- 104 | 105 | Please feel free to send Pull Requests to give back your improvements to the community! 106 | 107 | You can run the unit tests locally with just Docker installed using a simple alias: 108 | 109 | ```shell 110 | alias cm-cache-backend-file='docker run --rm -it -u $(id -u):$(id -g) -v ${COMPOSER_HOME:-$HOME/.composer}:/tmp -v $(pwd):/app --workdir /app cm-cache-backend-file' 111 | docker build . -t cm-cache-backend-file 112 | ``` 113 | 114 | Then, install Composer dependencies and run tests like so: 115 | ```shell 116 | cm-cache-backend-file composer install 117 | cm-cache-backend-file composer run-script test 118 | cm-cache-backend-file composer run-script php-cs-fixer -- --dry-run 119 | ``` 120 | 121 | Special Thanks 122 | -------------- 123 | 124 | Thanks to Vinai Kopp for the inspiring this backend with your symlink rendition! 125 | 126 | ``` 127 | @copyright Copyright (c) 2012 Colin Mollenhour (http://colin.mollenhour.com) 128 | This project is licensed under the "New BSD" license (see source). 129 | ``` 130 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"colinmollenhour/cache-backend-file", 3 | "type":"magento-module", 4 | "license":"BSD-3-Clause", 5 | "homepage":"https://github.com/colinmollenhour/Cm_Cache_Backend_File", 6 | "description":"The stock Zend_Cache_Backend_File backend has extremely poor performance for cleaning by tags making it become unusable as the number of cached items increases. This backend makes many changes resulting in a huge performance boost, especially for tag cleaning.", 7 | "authors":[ 8 | { 9 | "name":"Colin Mollenhour" 10 | } 11 | ], 12 | "require-dev": { 13 | "friendsofphp/php-cs-fixer": "^3.4", 14 | "phpunit/phpunit": "^9", 15 | "zf1s/zend-cache": "~1.15" 16 | }, 17 | "autoload": { 18 | "classmap": [ 19 | "File.php" 20 | ] 21 | }, 22 | "scripts": { 23 | "test": "vendor/bin/phpunit tests", 24 | "php-cs-fixer": "vendor/bin/php-cs-fixer fix --diff" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | File.php app/code/community/Cm/Cache/Backend/File.php 2 | -------------------------------------------------------------------------------- /tests/CommonBackend.php: -------------------------------------------------------------------------------- 1 | _className = $name; 40 | $this->_root = dirname(__FILE__); 41 | date_default_timezone_set('UTC'); 42 | parent::__construct($name, $data, $dataName); 43 | } 44 | 45 | public function setUp(): void 46 | { 47 | $this->mkdir(); 48 | if (false /*$notag*/) { 49 | $this->_instance->save('bar : data to cache', 'bar'); 50 | $this->_instance->save('bar2 : data to cache', 'bar2'); 51 | $this->_instance->save('bar3 : data to cache', 'bar3'); 52 | } else { 53 | $this->_instance->save('bar : data to cache', 'bar', array('tag3', 'tag4')); 54 | $this->_instance->save('bar2 : data to cache', 'bar2', array('tag3', 'tag1')); 55 | $this->_instance->save('bar3 : data to cache', 'bar3', array('tag2', 'tag3')); 56 | } 57 | } 58 | 59 | public function mkdir() 60 | { 61 | @mkdir($this->getTmpDir()); 62 | } 63 | 64 | public function rmdir() 65 | { 66 | $tmpDir = $this->getTmpDir(false); 67 | foreach (glob("$tmpDir*") as $dirname) { 68 | @rmdir($dirname); 69 | } 70 | } 71 | 72 | public function getTmpDir($date = true) 73 | { 74 | $suffix = ''; 75 | if ($date) { 76 | $suffix = date('mdyHis'); 77 | } 78 | if (is_writeable($this->_root)) { 79 | return $this->_root . DIRECTORY_SEPARATOR . 'zend_cache_tmp_dir_' . $suffix; 80 | } elseif (getenv('TMPDIR')) { 81 | return getenv('TMPDIR') . DIRECTORY_SEPARATOR . 'zend_cache_tmp_dir_' . $suffix; 82 | } else { 83 | die("no writable tmpdir found"); 84 | } 85 | } 86 | 87 | public function tearDown(): void 88 | { 89 | if ($this->_instance) { 90 | $this->_instance->clean(); 91 | } 92 | $this->rmdir(); 93 | } 94 | 95 | public function testConstructorCorrectCall() 96 | { 97 | $this->fail('PLEASE IMPLEMENT A testConstructorCorrectCall !!!'); 98 | } 99 | 100 | public function testConstructorBadOption() 101 | { 102 | $this->expectException('Zend_Cache_Exception'); 103 | $className = $this->_className; 104 | new $className(array(1 => 'bar')); 105 | } 106 | 107 | public function testSetDirectivesCorrectCall() 108 | { 109 | $this->_instance->setDirectives(array('lifetime' => 3600)); 110 | $this->assertTrue(true); 111 | } 112 | 113 | public function testSetDirectivesBadArgument() 114 | { 115 | $this->expectException('Zend_Cache_Exception'); 116 | $this->_instance->setDirectives('foo'); 117 | } 118 | 119 | public function testSetDirectivesBadDirective() 120 | { 121 | // A bad directive (not known by a specific backend) is possible 122 | // => so no exception here 123 | $this->_instance->setDirectives(array('foo' => true, 'lifetime' => 3600)); 124 | $this->assertTrue(true); 125 | } 126 | 127 | public function testSetDirectivesBadDirective2() 128 | { 129 | $this->expectException('Zend_Cache_Exception'); 130 | $this->_instance->setDirectives(array('foo' => true, 12 => 3600)); 131 | } 132 | 133 | public function testSaveCorrectCall() 134 | { 135 | $res = $this->_instance->save('data to cache', 'foo', array('tag1', 'tag2')); 136 | $this->assertTrue($res); 137 | } 138 | 139 | public function testSaveWithNullLifeTime() 140 | { 141 | $this->_instance->setDirectives(array('lifetime' => null)); 142 | $res = $this->_instance->save('data to cache', 'foo', array('tag1', 'tag2')); 143 | $this->assertTrue($res); 144 | } 145 | 146 | public function testSaveWithSpecificLifeTime() 147 | { 148 | $this->_instance->setDirectives(array('lifetime' => 3600)); 149 | $res = $this->_instance->save('data to cache', 'foo', array('tag1', 'tag2'), 10); 150 | $this->assertTrue($res); 151 | } 152 | 153 | public function testRemoveCorrectCall() 154 | { 155 | $this->assertTrue($this->_instance->remove('bar')); 156 | $this->assertFalse($this->_instance->test('bar')); 157 | $this->assertFalse($this->_instance->remove('barbar')); 158 | $this->assertFalse($this->_instance->test('barbar')); 159 | } 160 | 161 | public function testTestWithAnExistingCacheId() 162 | { 163 | $this->assertGreaterThan(999999, $this->_instance->test('bar')); 164 | } 165 | 166 | public function testTestWithANonExistingCacheId() 167 | { 168 | $this->assertFalse($this->_instance->test('barbar')); 169 | } 170 | 171 | public function testTestWithAnExistingCacheIdAndANullLifeTime() 172 | { 173 | $this->_instance->setDirectives(array('lifetime' => null)); 174 | $this->assertGreaterThan(999999, $this->_instance->test('bar')); 175 | } 176 | 177 | public function testGetWithANonExistingCacheId() 178 | { 179 | $this->assertFalse($this->_instance->load('barbar')); 180 | } 181 | 182 | public function testGetWithAnExistingCacheId() 183 | { 184 | $this->assertEquals('bar : data to cache', $this->_instance->load('bar')); 185 | } 186 | 187 | public function testGetWithAnExistingCacheIdAndUTFCharacters() 188 | { 189 | $data = '"""""' . "'" . '\n' . 'ééééé'; 190 | $this->_instance->save($data, 'foo'); 191 | $this->assertEquals($data, $this->_instance->load('foo')); 192 | } 193 | 194 | public function testGetWithAnExpiredCacheId() 195 | { 196 | $this->_instance->___expire('bar'); 197 | $this->_instance->setDirectives(array('lifetime' => -1)); 198 | $this->assertFalse($this->_instance->load('bar')); 199 | $this->assertEquals('bar : data to cache', $this->_instance->load('bar', true)); 200 | } 201 | 202 | public function testCleanModeAll() 203 | { 204 | $this->assertTrue($this->_instance->clean('all')); 205 | $this->assertFalse($this->_instance->test('bar')); 206 | $this->assertFalse($this->_instance->test('bar2')); 207 | } 208 | 209 | public function testCleanModeOld() 210 | { 211 | $this->_instance->___expire('bar2'); 212 | $this->assertTrue($this->_instance->clean('old')); 213 | $this->assertTrue($this->_instance->test('bar') > 999999); 214 | $this->assertFalse($this->_instance->test('bar2')); 215 | } 216 | 217 | public function testCleanModeMatchingTags() 218 | { 219 | $this->assertTrue($this->_instance->clean('matchingTag', array('tag3'))); 220 | $this->assertFalse($this->_instance->test('bar')); 221 | $this->assertFalse($this->_instance->test('bar2')); 222 | } 223 | 224 | public function testCleanModeMatchingTags2() 225 | { 226 | $this->assertTrue($this->_instance->clean('matchingTag', array('tag3', 'tag4'))); 227 | $this->assertFalse($this->_instance->test('bar')); 228 | $this->assertTrue($this->_instance->test('bar2') > 999999); 229 | } 230 | 231 | public function testCleanModeNotMatchingTags() 232 | { 233 | $this->assertTrue($this->_instance->clean('notMatchingTag', array('tag3'))); 234 | $this->assertTrue($this->_instance->test('bar') > 999999); 235 | $this->assertTrue($this->_instance->test('bar2') > 999999); 236 | } 237 | 238 | public function testCleanModeNotMatchingTags2() 239 | { 240 | $this->assertTrue($this->_instance->clean('notMatchingTag', array('tag4'))); 241 | $this->assertTrue($this->_instance->test('bar') > 999999); 242 | $this->assertFalse($this->_instance->test('bar2')); 243 | } 244 | 245 | public function testCleanModeNotMatchingTags3() 246 | { 247 | $this->assertTrue($this->_instance->clean('notMatchingTag', array('tag4', 'tag1'))); 248 | $this->assertTrue($this->_instance->test('bar') > 999999); 249 | $this->assertTrue($this->_instance->test('bar2') > 999999); 250 | $this->assertFalse($this->_instance->test('bar3')); 251 | } 252 | 253 | } 254 | -------------------------------------------------------------------------------- /tests/CommonExtendedBackend.php: -------------------------------------------------------------------------------- 1 | _capabilities = $this->_instance->getCapabilities(); 46 | } 47 | 48 | public function testGetFillingPercentage() 49 | { 50 | $res = $this->_instance->getFillingPercentage(); 51 | $this->assertTrue(is_integer($res)); 52 | $this->assertTrue($res >= 0); 53 | $this->assertTrue($res <= 100); 54 | } 55 | 56 | public function testGetFillingPercentageOnEmptyBackend() 57 | { 58 | $this->_instance->clean(Zend_Cache::CLEANING_MODE_ALL); 59 | $res = $this->_instance->getFillingPercentage(); 60 | $this->assertTrue(is_integer($res)); 61 | $this->assertTrue($res >= 0); 62 | $this->assertTrue($res <= 100); 63 | } 64 | 65 | public function testGetIds() 66 | { 67 | if (!$this->_capabilities['get_list']) { 68 | # unsupported by this backend 69 | return; 70 | } 71 | $res = $this->_instance->getIds(); 72 | $this->assertTrue(count($res) == 3); 73 | $this->assertTrue(in_array('bar', $res)); 74 | $this->assertTrue(in_array('bar2', $res)); 75 | $this->assertTrue(in_array('bar3', $res)); 76 | } 77 | 78 | public function testGetTags() 79 | { 80 | if (!$this->_capabilities['tags']) { 81 | # unsupported by this backend 82 | return; 83 | } 84 | $res = $this->_instance->getTags(); 85 | $this->assertEquals(4, count($res)); 86 | $this->assertTrue(in_array('tag1', $res)); 87 | $this->assertTrue(in_array('tag2', $res)); 88 | $this->assertTrue(in_array('tag3', $res)); 89 | $this->assertTrue(in_array('tag4', $res)); 90 | } 91 | 92 | public function testGetIdsMatchingTags() 93 | { 94 | if (!$this->_capabilities['tags']) { 95 | # unsupported by this backend 96 | return; 97 | } 98 | $res = $this->_instance->getIdsMatchingTags(array('tag3')); 99 | $this->assertTrue(count($res) == 3); 100 | $this->assertTrue(in_array('bar', $res)); 101 | $this->assertTrue(in_array('bar2', $res)); 102 | $this->assertTrue(in_array('bar3', $res)); 103 | } 104 | 105 | public function testGetIdsMatchingTags2() 106 | { 107 | if (!$this->_capabilities['tags']) { 108 | # unsupported by this backend 109 | return; 110 | } 111 | $res = $this->_instance->getIdsMatchingTags(array('tag2')); 112 | $this->assertTrue(count($res) == 1); 113 | $this->assertTrue(in_array('bar3', $res)); 114 | } 115 | 116 | public function testGetIdsMatchingTags3() 117 | { 118 | if (!$this->_capabilities['tags']) { 119 | # unsupported by this backend 120 | return; 121 | } 122 | $res = $this->_instance->getIdsMatchingTags(array('tag9999')); 123 | $this->assertTrue(count($res) == 0); 124 | } 125 | 126 | 127 | public function testGetIdsMatchingTags4() 128 | { 129 | if (!$this->_capabilities['tags']) { 130 | # unsupported by this backend 131 | return; 132 | } 133 | $res = $this->_instance->getIdsMatchingTags(array('tag3', 'tag4')); 134 | $this->assertTrue(count($res) == 1); 135 | $this->assertTrue(in_array('bar', $res)); 136 | } 137 | 138 | public function testGetIdsNotMatchingTags() 139 | { 140 | if (!$this->_capabilities['tags']) { 141 | # unsupported by this backend 142 | return; 143 | } 144 | $res = $this->_instance->getIdsNotMatchingTags(array('tag3')); 145 | $this->assertEquals(0, count($res)); 146 | } 147 | 148 | public function testGetIdsNotMatchingTags2() 149 | { 150 | if (!$this->_capabilities['tags']) { 151 | # unsupported by this backend 152 | return; 153 | } 154 | $res = $this->_instance->getIdsNotMatchingTags(array('tag1')); 155 | $this->assertTrue(count($res) == 2); 156 | $this->assertTrue(in_array('bar', $res)); 157 | $this->assertTrue(in_array('bar3', $res)); 158 | } 159 | 160 | public function testGetIdsNotMatchingTags3() 161 | { 162 | if (!$this->_capabilities['tags']) { 163 | # unsupported by this backend 164 | return; 165 | } 166 | $res = $this->_instance->getIdsNotMatchingTags(array('tag1', 'tag4')); 167 | $this->assertTrue(count($res) == 1); 168 | $this->assertTrue(in_array('bar3', $res)); 169 | } 170 | 171 | public function testGetMetadatas($notag = false) 172 | { 173 | $res = $this->_instance->getMetadatas('bar'); 174 | $this->assertTrue(isset($res['tags'])); 175 | $this->assertTrue(isset($res['mtime'])); 176 | $this->assertTrue(isset($res['expire'])); 177 | if ($notag) { 178 | $this->assertTrue(count($res['tags']) == 0); 179 | } else { 180 | $this->assertTrue(count($res['tags']) == 2); 181 | $this->assertTrue(in_array('tag3', $res['tags'])); 182 | $this->assertTrue(in_array('tag4', $res['tags'])); 183 | } 184 | $this->assertTrue($res['expire'] > time()); 185 | $this->assertTrue($res['mtime'] <= time()); 186 | } 187 | 188 | public function testTouch() 189 | { 190 | $res = $this->_instance->getMetadatas('bar'); 191 | $bool = $this->_instance->touch('bar', 30); 192 | $this->assertTrue($bool); 193 | $res2 = $this->_instance->getMetadatas('bar'); 194 | $this->assertTrue(($res2['expire'] - $res['expire']) == 30); 195 | $this->assertTrue($res2['mtime'] >= $res['mtime']); 196 | } 197 | 198 | public function testGetCapabilities() 199 | { 200 | $res = $this->_instance->getCapabilities(); 201 | $this->assertTrue(isset($res['tags'])); 202 | $this->assertTrue(isset($res['automatic_cleaning'])); 203 | $this->assertTrue(isset($res['expired_read'])); 204 | $this->assertTrue(isset($res['priority'])); 205 | $this->assertTrue(isset($res['infinite_lifetime'])); 206 | $this->assertTrue(isset($res['get_list'])); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /tests/FileBackendTest.php: -------------------------------------------------------------------------------- 1 | mkdir(); 50 | $this->_instance = new Cm_Cache_Backend_File(array( 51 | 'cache_dir' => $this->getTmpDir() . DIRECTORY_SEPARATOR, 52 | )); 53 | parent::setUp(); 54 | } 55 | 56 | public function tearDown(): void 57 | { 58 | parent::tearDown(); 59 | unset($this->_instance); 60 | } 61 | 62 | public function testConstructorBadOption() 63 | { 64 | $this->markTestSkipped(); 65 | } 66 | public function testConstructorCorrectCall() 67 | { 68 | $this->markTestSkipped(); 69 | } 70 | 71 | public function testGetWithANonExistingCacheIdAndANullLifeTime() 72 | { 73 | $this->_instance->setDirectives(array('lifetime' => null)); 74 | $this->assertFalse($this->_instance->load('barbar')); 75 | } 76 | 77 | public function testSaveCorrectCallWithHashedDirectoryStructure() 78 | { 79 | $this->_instance->setOption('hashed_directory_level', 2); 80 | $res = $this->_instance->save('data to cache', 'foo', array('tag1', 'tag2')); 81 | $this->assertTrue($res); 82 | } 83 | 84 | public function testCleanModeAllWithHashedDirectoryStructure() 85 | { 86 | $this->_instance->setOption('hashed_directory_level', 2); 87 | $this->assertTrue($this->_instance->clean('all')); 88 | $this->assertFalse($this->_instance->test('bar')); 89 | $this->assertFalse($this->_instance->test('bar2')); 90 | } 91 | 92 | public function testSaveWithABadCacheDir() 93 | { 94 | $this->_instance->setOption('cache_dir', '/foo/bar/lfjlqsdjfklsqd/'); 95 | $res = $this->_instance->save('data to cache', 'foo', array('tag1', 'tag2')); 96 | $this->assertFalse($res); 97 | } 98 | 99 | public function testSaveWithNullLifeTime2() 100 | { 101 | $res = $this->_instance->save('data to cache', 'foo', array('tag1', 'tag2'), null); 102 | $this->assertTrue($res); 103 | $metadatas = $this->_instance->getMetadatas('foo'); 104 | $this->assertGreaterThan(time() + 99999999, $metadatas['expire']); 105 | } 106 | } 107 | --------------------------------------------------------------------------------