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

Last change on this file since 516 was 516, checked in by sven, 5 years ago

Bug #51 gefixt (http://labs.technikum29.de/ticket/51).
Jetzt sollten neue Pagestyles gleich beim Erstellen erkannt werden.

File size: 10.9 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) {
42                // default values
43                $this->skip = isset($_GET['skip_cache']);
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         **/
125        function print_cache($ignore_http_caching=false) {
126                // make sure we already have called is_valid
127                if($this->mtime_cache_file === null)
128                        $this->is_valid(); // calculate mtime
129
130                if(!$this->debug) {
131                        header("Last-Modified: ".gmdate("D, d M Y H:i:s", $this->mtime_cache_file)." GMT");
132                        //header("Etag: $etag");
133                }
134
135                if(!$ignore_http_caching && @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $this->mtime_cache_file) {
136                        // client already has page cached locally
137                        if($this->debug) {
138                                print 'Would send Client a NOT MODIFIED answer.' . PHP_EOL;
139                        } else {
140                                header("HTTP/1.1 304 Not Modified");
141                                // important - no more output!
142                        }
143                } else {
144                        if($this->debug) {
145                                print 'Would send Client output of ' . $this->cache_file . PHP_EOL;
146                        } else {
147                                readfile($this->cache_file);
148                        }
149                }
150        }
151       
152        /**
153         * Convenience method which will exit the program after calling print_cache().
154         **/
155        function print_cache_and_exit() {
156                $this->print_cache();
157                exit;
158        }
159
160        /**
161         * Convenience method for calling the typical workflow: Test if the cache file
162         * shall be used, and if yes, print it out and exit the program. If this method
163         * returns, you can be sure that you have to create a (new) cache file. So your
164         * typical code will look like:
165         *
166         *  $cache = new t29Cache();
167         *  // initialization stuff $cache->... = ...
168         *  $cache->try_cache_and_exit();
169         *  // so we are still alive - go making content!
170         *  $cache->start_cache(...);
171         *  echo "be happy";
172         *  $cache->write_cache(); // at least if you didn't use any registered shutdown function.
173         *
174         **/
175        function try_cache_and_exit() {
176                if($this->shall_use())
177                        $this->print_cache_and_exit();
178        }
179
180        /**
181         * Start Output Buffering and prepare a register shutdown func,
182         * if wanted. Most likely you will call this method with arguments,
183         * otherwise it just calls ob_start() and that's it.
184         *
185         * TODO FIXME Doku outdated for this method --
186         *
187         * $register_shutdown_func can be:
188         *   - Just 'true': Then it will tell t29Cache to register it's
189         *     own write_cache() method as a shutdown function for PHP so
190         *     the cache file is written on script exit.
191         *   - A callable (method or function callable). This will be
192         *     registered at PHP shutdown, *afterwards* our own write_cache
193         *     method will be called. Thus you can inject printing some
194         *     footer code.
195         *   - A filter function. When $shutdown_func_is_filter is set to
196         *     some true value, your given callable $register_shutdown_func
197         *     will be used as a filter, thus being called with the whole
198         *     output buffer and expected to return some modification of that
199         *     stuff. Example:
200         *        $cache->start_cache(function($text) {
201         *          return strtoupper($text); }, true);
202         *     This will convert all page content to uppercase before saving
203         *     the stuff to the cache file.
204         **/
205        //function start_cache($register_shutdown_func=null, $shutdown_func_is_filter=false) {
206        function start_cache(array $args) {
207                $defaults = array(
208                        'shutdown_func' => null,
209                        'filter_func'   => null,
210                        'write_cache'   => true,
211                );
212                $args = array_merge($defaults, $args);
213               
214                if($this->debug)
215                        print "Will start caching with shutdown: " . $args['shutdown_func'] . PHP_EOL;
216                       
217                // check if output file is writable; for logging and logging output
218                // purpose.
219                //if(!is_writable($this->cache_file))
220                //      print "Cache file not writable: ".$this->cache_file;
221                //      print "\n";
222                //exit;
223
224                ob_start();
225
226                if($args['shutdown_func'] || $args['filter_func']) {
227                        // callback/filter given: Register a shutdown function
228                        // which will call user's callback at first, then
229                        // our own write function. and which handles filters
230                        $t = $this; // PHP stupidity
231                        register_shutdown_function(function()  use($args, $t) {
232                                if($args['filter_func']) {
233                                        // also collect the shutdown func prints in the $content
234                                        if($args['shutdown_func'])
235                                                call_user_func($args['shutdown_func']);
236
237                                        $content = ob_get_clean();
238                                        if($t->debug)
239                                                // can print output since OutputBuffering is finished
240                                                print 't29Cache: Applying user filter to output' . PHP_EOL;
241                                        $content = call_user_func($args['filter_func'], $content);
242                                        print $content;
243                                               
244                                        if($args['write_cache'])
245                                                $t->write_cache($content);
246                                        return;
247                                } else if($args['shutdown_func'])
248                                        call_user_func($args['shutdown_func']);
249                                if($args['write_cache'])
250                                        $t->write_cache();
251                        });
252                } elseif($args['write_cache']) {
253                        // Just register our own write function
254                        register_shutdown_function(array($this, 'write_cache'));
255                } else {
256                        // nothing given: Dont call our write function,
257                        // it must therefore be called by hand.
258                }
259        }
260
261        /**
262         * Write Cache file. If the $content string is given, it will
263         * be used as the cache content. Otherwise, a running output buffering
264         * will be assumed (as start_cache fires it) and content will be
265         * extracted with ob_get_flush.
266         * @param $content Content to be used as cache content or OB content
267         * @param $clear_ob_cache Use ob_get_clean instead of flushing it. If given,
268         *                        will return $content instead of printing/keeping it.
269         **/
270        function write_cache($content=null, $clear_ob_cache=false) {
271                if(!$content)
272                        $content = ($clear_ob_cache ? ob_get_clean() : ob_get_flush());
273
274                if($this->skip) {
275                        $this->print_info('skipped cache and cache saving.');
276                        return; // do not save anything.
277                }
278               
279                if(!file_exists($this->cache_file)) {
280                        if(!self::mkdir_recursive(dirname($this->cache_file)))
281                                $this->print_error('Could not create recursive caching directories');
282                }
283               
284                if(@file_put_contents($this->cache_file, $content))
285                        $this->print_info('Wrote output cache successfully');
286                else
287                        $this->print_error('Could not write page output cache to '.$this->cache_file);
288
289                if($clear_ob_cache)
290                        return $content;
291        }
292       
293       
294        private function print_info($string, $even_if_nonverbose=false) {
295                if($this->verbose || $even_if_nonverbose)
296                        echo "<!-- t29Cache: $string -->\n";
297        }
298       
299        private function print_error($string, $even_if_nonverbose=false) {
300                require_once dirname(__FILE__).'/logging.php';
301                $log = t29Log::get();
302               
303                if($this->verbose || $even_if_nonverbose)
304                        $log->WARN("t29Cache: ".$string, t29Log::IMMEDIATELY_PRINT);
305        }
306}
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