Last Updated: September 09, 2019
·
3.159K
· nirgavish

PHP caching without headaches

This is a caching mechanism I made just as a preliminary test, but it's planned to make it into the next release of Apex, my framework, once it is properly tested. This class is set to replace virtually all other caching mechanisms currently in the distribution.

It supports function-return caching and HTML-fragment caching.

It is a single object with 2 methods, accessed statically, that is roughly 20% the size of my current caching code. Hope you find it useful, but if there are any issues or bugs, I'd love to hear about them.

Usage:

Creating and accessing a cache

$q = cache::make('my_cache', 5, function(){
    print 'This is cached for 5 minutes';
    return 'So it this';
}); // will immediately print 'This is cached'.

print $q; // will print 'So is this'.

Destroying a cache on-the-fly (prematurely)

// function 'destroy()' supports wildcards
cache::destroy('my_cache');

Full code

class cache {
    /**
     * $cache_folder is the path to the cache folder.
     * All cache files and folders will be created inside this folder
     *
     * You may set this value from the outside to change the path:
     * cache::$cache_folder - 'some/other/cache/folder';
     * */
    public static $cache_folder = 'cache/';

        /**
         * $last_action is defaulted at NULL.
         * Every time the caching mechanism is used, this value is updated
         * to the last action taken by the cache:: class
         * These values may be either of the 2 class constants:
         * USE_CACHED_FILE or CACHE_FILE_NOW
         * */
        public static $last_action = null;

        const USE_CACHED_FILE = 1;
        const CACHE_FILE_NOW = 2;

        /**
         * This function is accesesed statically
         * cache::make( $name, $expires, $function );
         *
         * $name
         * =====
         * $name refers to the filename of the cache you're currently
         * making, it may be something like user_12_messages or
         * all_members or any string that may be used as a filename.
         * You may set this to a path, ie: users/12/messages
         *
         * $expires
         * ========
         * Number of minutes that the store will keep the cache alive, once
         * this time is exceeded, the mechanism will re-cache your code.
         *
         * $function
         * =========
         * This argument is a closure function containing whatever it is
         * you want to cache The caching mechanism will cache both
         * the function return and any echos and prints to the buffer,
         * so it may be used to cache both data and html.
         *
         * Example
         * =======
         * $q = cahce::make('my_cache', 5, function(){
         *     print 'This is cached';
         *     return 'So it this';
         * }); // will immediately print 'This is cached'.
         *
         * print $q; // will print 'So is this'.
         * 
         * */
        static function make($name, $expires, $function){

            $cache_path = self::$cache_folder.$name.'.json';

            $action = self::USE_CACHED_FILE;


            if(!file_exists(dirname($cache_path))){
                    mkdir(dirname($cache_path), 0777, true);
                }

            if(!file_exists($cache_path)){
                $action = self::CACHE_FILE_NOW;
            }elseif( (time()-filemtime($cache_path))>$expires*60 && $expires!=0 ){
                $action = self::CACHE_FILE_NOW;
            }

            switch($action){
                case self::USE_CACHED_FILE:
                    $cache = json_decode(file_get_contents($cache_path));
                    break;
                case self::CACHE_FILE_NOW:
                    ob_start();
                    $cache = array($function(), ob_get_clean());

                    $fp = fopen($cache_path, "c");
                    if (flock($fp, LOCK_EX) ){
                        // File locked and ready
                        fwrite($fp, json_encode($cache));
                        flock($fp, LOCK_UN);
                    } 
                    fclose($fp);

                    break;
            }

            self::$last_action = $action;
            echo $cache[1];
            return $cache[0];
        }

        /**
         * This function is used to prematurely destroy an existing cache.
         * cache::destroy($name);
         *
         * $name
         * =====
         * The name (or full path) of the cache to be destroyed.
         * The name mey contain the "*" wildcard.
         *
         * Example
         * =======
         * cache::destroy('side_navigation');
         *
         * or
         *
         * cache::destroy('user_*_notifications');
         *
         * or even
         *
         * cache::destroy('user/*');
         * */
        static function destroy($name){
            if(strpos($name,'*')==-1){
                @unlink(CACHE_FOLDER.'/'.$name);
            }else{
                foreach (glob(CACHE_FOLDER.'/'.$name) as $filename) {
                    @unlink($filename);
                }
            }
        }
    }
    ```

4 Responses
Add your response

Nice, though I see some potential problems here I'd recommend to fix before putting it into Apex

Unix issues:

1. mkdir 777

Code issues:

2. action = null, next line action = something

3. if and elseif that does the same should be replaced by OR

4. storing last action is useless, and php is stateless

Concurrency issues

4. fopen(.., 'c') doesnt truncate file, therefore new shorter cached content will replace old one only partially, forming an incorrect file content and invalid json

5.flock + fwrite in userland is way slower then just fileputcontents and same logic implemented in C land. May be critical and if someone decides to intentionally load a page using it multiple times in parallel.

6. fclose outside of try-finally may never happen, same about flock(lock_un). critical if you suddenly decide to use this in a command line app.

7. destroy traverses directory instead of directly accessing file. Very IO expensive and performance slow

General issues:

8. hardcoded config (cache dir)

9. no parameters validation. $expire can be not int and name can be "../../index.php".

10. error suppressing with @

Architecture issues:

11. Static is not testable as every test case will have side effects on other classes

12. It's not injectable as dependency, therefore other classes will result strongly coupled on it.

13. It is not replaceable. You cannot substitute file cache with apc cache when time comes.

Woot?

14. File as cache? You really don't have apc or memcache ?

15. There are more comments then code (and more issues then code as well)

16. One of the strongest parts of php is the wide range of libraries ready out there. Just use of of the existing caching implementations.

Other then that, not bad ;)

over 1 year ago ·

1) PHP manual states 0777, not 777: http://php.net/manual/en/function.mkdir.php - if you are aware of any distinction, please share.

2) $action = null; is a residual line, shouldn't be there, fixed, thanks.

3) Incorrect, using the elsif as a condition in the first 'if' will yield an error if the file doesn't exist.

4) The last action is meant for you to be able to determine if a cache was used or not from the same script, has nothing to do with PHP being stateless.

4(2)-6) What are you suggesting? Solution? I suspect you tried to manually fail the flock($fp, LOCK_EX) condition, which will not give you the same results as when the file really DID fail to flock.

7) Destroy does not traverse directories, it unlinks the file directly if no wildcard is present, and unlinks multiple files if a wildcard IS present.

8) You're correct, this is not ideal: Hardcoded for simplicity, when implemented in the framework, it will have a config value in the config file, and in any case, you can change this value at runtime as much as you want.

9) I don't tend to handle function input when it's only exposed to the developer. It would be the same as saying the developer can unlink('index.php') - Yup, they sure can.

10) You're right, shouldn't be there in a finished class, but will stay there for now.

11) It's a long debate, I'm not particularly fond of static, and Apex has instances of libraries, not static calls, but it seemed more appropriate here.

12) -"-

13) -"-

14) Yes, really. APC and memcacheD have my respect, and I intend the finished version to have a switch for file, database, apc, and memcached - but for now this is the POC. I have used filesystem caching with great success in places where the other methods were not viable.

15) First time I've ever gotten a complaint over too much documentation.

16) Thanks.

Other than that, good points :)

over 1 year ago ·

Nice, keep up the good work! Just a minor typo on the first code block (creating and accessing a cache) - should be cache instead of cahce :-)

over 1 year ago ·

Thanks, fixed.

over 1 year ago ·