├── .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 |
--------------------------------------------------------------------------------