source: t29-www/lib/cache.php @ 1206

Last change on this file since 1206 was 1106, checked in by heribert, 7 years ago

Bugfix im Cache-System: Bei interaktiven Seiten versuchte der Footer-Cache bislang, HTTP-Header auszusenden was in Warnungen resultierte. Fehler beseitigt, Warnungen sind weg.

File size: 11.2 KB
Line 
1<?php
2/**
3 * Main caching and output system.
4 * Modes:
5 *  SKIP  = Skips any caching. By enabling this you just disable
6 *          the caching system which can be great for testing.
7 *          use_cache() will always return false and write_cache()
8 *          will just skip any work.
9 *
10 *  PURGE = Enforce a rewrite of the cache. use_cache() will always
11 *          return false and therefore the cache file will be rewritten
12 *          as if it didn't exist.
13 *
14 *  DEBUG = Debug cache validation calculation and output calculation.
15 *          This will print verbose data just whenever called. The
16 *          cache system can be run around that, but "NOT CHANGED" calls
17 *          will just abort the scenery. Somewhat weird.
18 *
19 *  VERBOSE = Print verbose Messages as HTML Comments (print_info)
20 *            or error messages in div spans. This is useful for any
21 *            HTML output but should be disabled for JS/CSS caching.
22 **/
23
24# Lightweight caching system
25class t29Cache {
26        const webroot_cache_dir = '/shared/cache'; # relative to webroot
27
28        public $skip = false;
29        public $purge = false;
30        public $debug = false;// debug calculation
31        public $verbose = false; // print html comments and errors
32
33        // these must be set after constructing!
34        public $cache_file; // must be set!
35        public $test_files = array(); // must be set!
36        public $test_conditions = array(); // can be filled with booleans
37
38        private $mtime_cache_file = null; // needed for cache header output
39        private $is_valid = null; // cache output value
40
41        function __construct($debug=false, $verbose=false, $skip=false) {
42                // default values
43                $this->skip = isset($_GET['skip_cache']) || $skip;
44                $this->purge = isset($_GET['purge_cache']);
45                $this->debug = isset($_GET['debug_cache']) || $debug;
46                $this->verbose = isset($_GET['verbose_cache']) || $verbose || $this->debug;
47        }
48       
49        /**
50         * expecting:
51         * @param $webroot /var/www/foo/bar  (no trailing slash!)
52         * @param $filename /de/bar/baz.htm  (starting with slash!)
53         * @returns absolute filename /var/www/foo/bar/cache/dir/de/bar/baz.htm
54         **/
55        function set_cache_file($webroot, $filename) {
56                $this->cache_file = $webroot . self::webroot_cache_dir . '/' . $filename;
57        }
58       
59        # helper function
60        public static function mkdir_recursive($pathname) {
61                is_dir(dirname($pathname)) || self::mkdir_recursive(dirname($pathname));
62                return is_dir($pathname) || @mkdir($pathname);
63        }
64
65        /**
66         * Calculates and compares the mtimes of the cache file and testing files.
67         * Doesn't obey any debug/skip/purge rules, just gives out if the cache file
68         * is valid or not.
69         * The result is cached in $is_valid, so you can call this (heavy to calc)
70         * method frequently.
71         **/
72        function is_valid() {
73                // no double calculation
74                if($this->is_valid !== null) return $this->is_valid;
75
76                if($this->debug) {
77                        print '<pre>';
78                        print 't29Cache: Validity Checking.'.PHP_EOL;
79                        print 'Cache file: '; var_dump($this->cache_file);
80                        print 'Test files: '; var_dump($this->test_files);
81                        print 'Test conditions: '; var_dump($this->test_conditions);
82                }
83
84                $this->mtime_cache_file = @filemtime($this->cache_file);
85                $mtime_test_files = array_map(
86                        function($x){return @filemtime($x);},
87                        $this->test_files);
88                $mtime_test_max = array_reduce($mtime_test_files, 'max');
89                // new feature: Testing boolean conditions. If $this->test_conditions is
90                // an empty array, the calculation gives true.
91                $test_conditions = array_reduce($this->test_conditions, function($a,$b){ return $a && $b; }, true);
92                $this->is_valid = $this->mtime_cache_file
93                        && $mtime_test_max < $this->mtime_cache_file && $test_conditions;
94                       
95                if($this->debug) {
96                        print 'Cache mtime: '; var_dump($this->mtime_cache_file);
97                        print 'Test files mtimes: '; var_dump($mtime_test_files);
98                        print 'CACHE IS VALID: '; var_dump($this->is_valid);
99                }
100
101                return $this->is_valid;
102        }
103
104        /**
105         * The "front end" to is_valid: Takes skipping and purging rules into
106         * account to decide whether you shall use the cache or not.
107         * @returns boolean value if cache is supposed to be valid or not.
108         **/
109        function shall_use() {
110                $test = $this->is_valid() && !$this->skip && !$this->purge;
111                if($this->debug) {
112                        print 'Shall use Cache: '; var_dump($test);
113                }
114                return $test;
115        }
116       
117        /**
118         * Prints out cache file with according HTTP headers and HTTP caching
119         * (HTTP_IF_MODIFIED_SINCE). You must not print out anything after such a http
120         * header! Therefore consider using the convenience method print_cache_and_exit()
121         * instead of this one or exit on yourself.
122         *
123         * @param $ignore_http_caching Don't check the clients HTTP cache
124         * @param $skip_http_headers Don't send HTTP headers. Used for instance in Footer cache.
125         **/
126        function print_cache($ignore_http_caching=false, $skip_http_headers=false) {
127                // make sure we already have called is_valid
128                if($this->mtime_cache_file === null)
129                        $this->is_valid(); // calculate mtime
130
131                if(!$skip_http_headers) {
132                        if(!$this->debug) {
133                                header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->mtime_cache_file)." GMT");
134                                //header("Etag: $etag");
135                        }
136
137                        if(!$ignore_http_caching && @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->mtime_cache_file) {
138                                // client already has page cached locally
139                                if($this->debug) {
140                                        print 'Would send Client a NOT MODIFIED answer.' . PHP_EOL;
141                                } else {
142                                        header("HTTP/1.1 304 Not Modified");
143                                        // important - no more output!
144                                }
145                                return;
146                        }
147                } else {
148                        if($this->debug)
149                                print 'Skipping HTTP Headers as requested' . PHP_EOL;
150                }
151               
152                if($this->debug) {
153                        print 'Would send Client output of ' . $this->cache_file . PHP_EOL;
154                } else {
155                        readfile($this->cache_file);
156                }
157        }
158       
159        /**
160         * Convenience method which will exit the program after calling print_cache().
161         **/
162        function print_cache_and_exit() {
163                $this->print_cache();
164                exit;
165        }
166
167        /**
168         * Convenience method for calling the typical workflow: Test if the cache file
169         * shall be used, and if yes, print it out and exit the program. If this method
170         * returns, you can be sure that you have to create a (new) cache file. So your
171         * typical code will look like:
172         *
173         *  $cache = new t29Cache();
174         *  // initialization stuff $cache->... = ...
175         *  $cache->try_cache_and_exit();
176         *  // so we are still alive - go making content!
177         *  $cache->start_cache(...);
178         *  echo "be happy";
179         *  $cache->write_cache(); // at least if you didn't use any registered shutdown function.
180         *
181         **/
182        function try_cache_and_exit() {
183                if($this->shall_use())
184                        $this->print_cache_and_exit();
185        }
186
187        /**
188         * Start Output Buffering and prepare a register shutdown func,
189         * if wanted. Most likely you will call this method with arguments,
190         * otherwise it just calls ob_start() and that's it.
191         *
192         * TODO FIXME Doku outdated for this method --
193         *
194         * $register_shutdown_func can be:
195         *   - Just 'true': Then it will tell t29Cache to register it's
196         *     own write_cache() method as a shutdown function for PHP so
197         *     the cache file is written on script exit.
198         *   - A callable (method or function callable). This will be
199         *     registered at PHP shutdown, *afterwards* our own write_cache
200         *     method will be called. Thus you can inject printing some
201         *     footer code.
202         *   - A filter function. When $shutdown_func_is_filter is set to
203         *     some true value, your given callable $register_shutdown_func
204         *     will be used as a filter, thus being called with the whole
205         *     output buffer and expected to return some modification of that
206         *     stuff. Example:
207         *        $cache->start_cache(function($text) {
208         *          return strtoupper($text); }, true);
209         *     This will convert all page content to uppercase before saving
210         *     the stuff to the cache file.
211         **/
212        //function start_cache($register_shutdown_func=null, $shutdown_func_is_filter=false) {
213        function start_cache(array $args) {
214                $defaults = array(
215                        'shutdown_func' => null,
216                        'filter_func'   => null,
217                        'write_cache'   => true,
218                );
219                $args = array_merge($defaults, $args);
220               
221                if($this->debug)
222                        print "Will start caching with shutdown: " . $args['shutdown_func'] . PHP_EOL;
223                       
224                // check if output file is writable; for logging and logging output
225                // purpose.
226                //if(!is_writable($this->cache_file))
227                //      print "Cache file not writable: ".$this->cache_file;
228                //      print "\n";
229                //exit;
230
231                ob_start();
232
233                if($args['shutdown_func'] || $args['filter_func']) {
234                        // callback/filter given: Register a shutdown function
235                        // which will call user's callback at first, then
236                        // our own write function. and which handles filters
237                        $t = $this; // PHP stupidity
238                        register_shutdown_function(function()  use($args, $t) {
239                                if($args['filter_func']) {
240                                        // also collect the shutdown func prints in the $content
241                                        if($args['shutdown_func'])
242                                                call_user_func($args['shutdown_func']);
243
244                                        $content = ob_get_clean();
245                                        if($t->debug)
246                                                // can print output since OutputBuffering is finished
247                                                print 't29Cache: Applying user filter to output' . PHP_EOL;
248                                        $content = call_user_func($args['filter_func'], $content);
249                                        print $content;
250                                               
251                                        if($args['write_cache'])
252                                                $t->write_cache($content);
253                                        return;
254                                } else if($args['shutdown_func'])
255                                        call_user_func($args['shutdown_func']);
256                                if($args['write_cache'])
257                                        $t->write_cache();
258                        });
259                } elseif($args['write_cache']) {
260                        // Just register our own write function
261                        register_shutdown_function(array($this, 'write_cache'));
262                } else {
263                        // nothing given: Dont call our write function,
264                        // it must therefore be called by hand.
265                }
266        }
267
268        /**
269         * Write Cache file. If the $content string is given, it will
270         * be used as the cache content. Otherwise, a running output buffering
271         * will be assumed (as start_cache fires it) and content will be
272         * extracted with ob_get_flush.
273         * @param $content Content to be used as cache content or OB content
274         * @param $clear_ob_cache Use ob_get_clean instead of flushing it. If given,
275         *                        will return $content instead of printing/keeping it.
276         **/
277        function write_cache($content=null, $clear_ob_cache=false) {
278                if(!$content)
279                        $content = ($clear_ob_cache ? ob_get_clean() : ob_get_flush());
280
281                if($this->skip) {
282                        $this->print_info('skipped cache and cache saving.');
283                        //return; // do not save anything.
284                } else {
285                        if(!file_exists($this->cache_file)) {
286                                if(!self::mkdir_recursive(dirname($this->cache_file)))
287                                        $this->print_error('Could not create recursive caching directories');
288                        }
289                       
290                        if(@file_put_contents($this->cache_file, $content))
291                                $this->print_info('Wrote output cache successfully');
292                        else
293                                $this->print_error('Could not write page output cache to '.$this->cache_file);
294                }
295
296                if($clear_ob_cache)
297                        return $content;
298        }
299       
300       
301        private function print_info($string, $even_if_nonverbose=false) {
302                if($this->verbose || $even_if_nonverbose)
303                        echo "<!-- t29Cache: $string -->\n";
304        }
305       
306        private function print_error($string, $even_if_nonverbose=false) {
307                require_once dirname(__FILE__).'/logging.php';
308                $log = t29Log::get();
309               
310                if($this->verbose || $even_if_nonverbose)
311                        $log->WARN("t29Cache: ".$string, t29Log::IMMEDIATELY_PRINT);
312        }
313}
Note: See TracBrowser for help on using the repository browser.
© 2008 - 2013 technikum29 • Sven Köppel • Some rights reserved
Powered by Trac
Expect where otherwise noted, content on this site is licensed under a Creative Commons 3.0 License