1 <?php
2 /**
3 * SS_Cache provides a bunch of static functions wrapping the Zend_Cache system in something a little more
4 * easy to use with the SilverStripe config system.
5 *
6 * A Zend_Cache has both a frontend (determines how to get the value to cache, and how to serialize it for storage)
7 * and a backend (handles the actual storage).
8 *
9 * Rather than require library code to specify the backend directly, cache consumers provide a name for the cache
10 * backend they want. The end developer can then specify which backend to use for each name in their project's
11 * _config.php. They can also use 'all' to provide a backend for all named caches
12 *
13 * End developers provide a set of named backends, then pick the specific backend for each named cache. There is a
14 * default File cache set up as the 'default' named backend, which is assigned to 'all' named caches
15 *
16 * USING A CACHE
17 *
18 * $cache = SS_Cache::factory('foo') ; // foo is any name (try to be specific), and is used to get configuration & storage info
19 *
20 * if (!($result = $cache->load($cachekey))) {
21 * $result = caluate some how;
22 * $cache->save($result);
23 * }
24 *
25 * return $result;
26 *
27 * Normally there's no need to remove things from the cache - the cache backends clear out entries based on age & maximum
28 * allocated storage. If you include the version of the object in the cache key, even object changes don't need any invalidation
29 *
30 * DISABLING CACHING IN DEV MOVE
31 *
32 * (in _config.php)
33 *
34 * if (Director::isDev()) SS_Cache::set_cache_lifetime('any', -1, 100);
35 *
36 * USING MEMCACHED AS STORE
37 *
38 * (in _config.php)
39 *
40 * SS_Cache::add_backend('primary_memcached', 'Memcached',
41 * array('host' => 'localhost', 'port' => 11211, 'persistent' => true, 'weight' => 1, 'timeout' => 5, 'retry_interval' => 15, 'status' => true, 'failure_callback' => '' )
42 * );
43 *
44 * SS_Cache::pick_backend('primary_memcached', 'any', 10);
45 * SS_Cache::pick_backend('default', 'aggregate', 20); // Aggregate needs a backend with tag support, which memcached doesn't provide
46 *
47 * USING APC AND FILE AS TWO LEVEL STORE
48 *
49 * (in _config.php)
50 *
51 * SS_Cache::add_backend('two-level', 'TwoLevels' array(
52 * 'slow_backend' => 'File',
53 * 'fast_backend' => 'Apc',
54 * 'slow_backend_options' => array('cache_dir' => TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache')
55 * ));
56 *
57 * SS_Cache::pick_backend('two-level', 'any', 10); // No need for special backend for aggregate - TwoLevels with a File slow backend supports tags
58 *
59 * @author hfried
60 * @package sapphire
61 * @subpackage core
62 */
63 class SS_Cache {
64
65 protected static $backends = array();
66 protected static $backend_picks = array();
67
68 protected static $cache_lifetime = array();
69
70 /**
71 * Initialize the 'default' named cache backend
72 */
73 protected static function init(){
74 if (!isset(self::$backends['default'])) {
75 $cachedir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache';
76 if (!is_dir($cachedir)) mkdir($cachedir);
77 self::$backends['default'] = array('File', array('cache_dir' => TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache'));
78 }
79 }
80
81 /**
82 * Add a new named cache backend
83 *
84 * @param string $name The name of this backend as a freeform string
85 * @param string $type The Zend_Cache backend ('File' or 'Sqlite' or ...)
86 * @param array $options The Zend_Cache backend options (see http://framework.zend.com/manual/en/zend.cache.html)
87 * @return none
88 */
89 static function add_backend($name, $type, $options=array()) {
90 self::init();
91 self::$backends[$name] = array($type, $options);
92 }
93
94 /**
95 * Pick a named cache backend for a particular named cache
96 *
97 * @param string $name The name of the backend, as passed as the first argument to add_backend
98 * @param string $for The name of the cache to pick this backend for (or 'any' for any backend)
99 * @param integer $priority The priority of this pick - the call with the highest number will be the actual backend picked.
100 * A backend picked for a specific cache name will always be used instead of 'any' if it exists, no matter the priority.
101 * @return none
102 */
103 static function pick_backend($name, $for, $priority=1) {
104 self::init();
105
106 $current = -1;
107 if (isset(self::$backend_picks[$for])) $current = self::$backend_picks[$for]['priority'];
108
109 if ($priority >= $current) self::$backend_picks[$for] = array('name' => $name, 'priority' => $priority);
110 }
111
112 /**
113 * Set the cache lifetime for a particular named cache
114 *
115 * @param string $for The name of the cache to set this lifetime for (or 'any' for all backends)
116 * @param integer $lifetime The lifetime of an item of the cache, in seconds, or -1 to disable caching
117 * @param integer $priority The priority. The highest priority setting is used. Unlike backends, 'any' is not special in terms of priority.
118 */
119 static function set_cache_lifetime($for, $lifetime=600, $priority=1) {
120 self::init();
121
122 $current = -1;
123 if (isset(self::$cache_lifetime[$for])) $current = self::$cache_lifetime[$for]['priority'];
124
125 if ($priority >= $current) self::$cache_lifetime[$for] = array('lifetime' => $lifetime, 'priority' => $priority);
126 }
127
128 /**
129 * Build a cache object
130 * @param string $for The name of the cache to build the cache object for
131 * @param string $frontend (optional) The type of Zend_Cache frontend to use. Output is almost always the best
132 * @param array $frontendOptions (optional) Any frontend options to use.
133 *
134 * @return The cache object. It has been partitioned so that actions on the object
135 * won't affect cache objects generated with a different $for value, even those using the same Zend_Backend
136 *
137 * -- Cache a calculation
138 *
139 * if (!($result = $cache->load($cachekey))) {
140 * $result = caluate some how;
141 * $cache->save($result);
142 * }
143 *
144 * return $result;
145 *
146 * -- Cache captured output
147 *
148 * if (!($cache->start($cachekey))) {
149 *
150 * // output everything as usual
151 * echo 'Hello world! ';
152 * echo 'This is cached ('.time().') ';
153 *
154 * $cache->end(); // output buffering ends
155 * }
156 *
157 * -- Invalidate an element
158 *
159 * $cache->remove($cachekey);
160 *
161 * -- Clear the cache (warning - this clears the entire backend, not just this named cache partition)
162 *
163 * $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
164 *
165 * See the Zend_Cache documentation at http://framework.zend.com/manual/en/zend.cache.html for more
166 *
167 */
168 static function factory($for, $frontend='Output', $frontendOptions=null) {
169 self::init();
170
171 $backend_name = 'default'; $backend_priority = -1;
172 $cache_lifetime = 600; $lifetime_priority = -1;
173
174 foreach (array('any', $for) as $name) {
175 if (isset(self::$backend_picks[$name]) && self::$backend_picks[$name]['priority'] > $backend_priority) {
176 $backend_name = self::$backend_picks[$name]['name'];
177 $backend_priority = self::$backend_picks[$name]['priority'];
178 }
179
180 if (isset(self::$cache_lifetime[$name]) && self::$cache_lifetime[$name]['priority'] > $lifetime_priority) {
181 $cache_lifetime = self::$cache_lifetime[$name]['lifetime'];
182 $lifetime_priority = self::$cache_lifetime[$name]['priority'];
183 }
184 }
185
186 $backend = self::$backends[$backend_name];
187
188 $basicOptions = array('cache_id_prefix' => $for);
189
190 if ($cache_lifetime >= 0) $basicOptions['lifetime'] = $cache_lifetime;
191 else $basicOptions['caching'] = false;
192
193 $frontendOptions = $frontendOptions ? array_merge($basicOptions, $frontendOptions) : $basicOptions;
194
195 require_once 'Zend/Cache.php';
196 return Zend_Cache::factory($frontend, $backend[0], $frontendOptions, $backend[1]);
197 }
198 }