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 |
---|
25 | class 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 | } |
---|