1: <?php
2: /**
3: * COutputCache class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * COutputCache enables caching the output generated by an action or a view fragment.
13: *
14: * If the output to be displayed is found valid in cache, the cached
15: * version will be displayed instead, which saves the time for generating
16: * the original output.
17: *
18: * Since COutputCache extends from {@link CFilterWidget}, it can be used
19: * as either a filter (for action caching) or a widget (for fragment caching).
20: * For the latter, the shortcuts {@link CBaseController::beginCache()} and {@link CBaseController::endCache()}
21: * are often used instead, like the following in a view file:
22: * <pre>
23: * if($this->beginCache('cacheName',array('property1'=>'value1',...))
24: * {
25: * // ... display the content to be cached here
26: * $this->endCache();
27: * }
28: * </pre>
29: *
30: * COutputCache must work with a cache application component specified via {@link cacheID}.
31: * If the cache application component is not available, COutputCache will be disabled.
32: *
33: * The validity of the cached content is determined based on two factors:
34: * the {@link duration} and the cache {@link dependency}.
35: * The former specifies the number of seconds that the data can remain
36: * valid in cache (defaults to 60s), while the latter specifies conditions
37: * that the cached data depends on. If a dependency changes,
38: * (e.g. relevant data in DB are updated), the cached data will be invalidated.
39: * For more details about cache dependency, see {@link CCacheDependency}.
40: *
41: * Sometimes, it is necessary to turn off output caching only for certain request types.
42: * For example, we only want to cache a form when it is initially requested;
43: * any subsequent display of the form should not be cached because it contains user input.
44: * We can set {@link requestTypes} to be <code>array('GET')</code> to accomplish this task.
45: *
46: * The content fetched from cache may be variated with respect to
47: * some parameters. COutputCache supports four kinds of variations:
48: * <ul>
49: * <li>{@link varyByRoute}: this specifies whether the cached content
50: * should be varied with the requested route (controller and action)</li>
51: * <li>{@link varyByParam}: this specifies a list of GET parameter names
52: * and uses the corresponding values to determine the version of the cached content.</li>
53: * <li>{@link varyBySession}: this specifies whether the cached content
54: * should be varied with the user session.</li>
55: * <li>{@link varyByExpression}: this specifies whether the cached content
56: * should be varied with the result of the specified PHP expression.</li>
57: * <li>{@link varyByLanguage}: this specifies whether the cached content
58: * should by varied with the user's language. Available since 1.1.14.</li>
59: * </ul>
60: * For more advanced variation, override {@link getBaseCacheKey()} method.
61: *
62: * @property boolean $isContentCached Whether the content can be found from cache.
63: *
64: * @author Qiang Xue <qiang.xue@gmail.com>
65: * @package system.web.widgets
66: * @since 1.0
67: */
68: class COutputCache extends CFilterWidget
69: {
70: /**
71: * Prefix to the keys for storing cached data
72: */
73: const CACHE_KEY_PREFIX='Yii.COutputCache.';
74:
75: /**
76: * @var integer number of seconds that the data can remain in cache. Defaults to 60 seconds.
77: * If it is 0, existing cached content would be removed from the cache.
78: * If it is a negative value, the cache will be disabled (any existing cached content will
79: * remain in the cache.)
80: *
81: * Note, if cache dependency changes or cache space is limited,
82: * the data may be purged out of cache earlier.
83: */
84: public $duration=60;
85: /**
86: * @var boolean whether the content being cached should be differentiated according to route.
87: * A route consists of the requested controller ID and action ID.
88: * Defaults to true.
89: */
90: public $varyByRoute=true;
91: /**
92: * @var boolean whether the content being cached should be differentiated according to user sessions. Defaults to false.
93: */
94: public $varyBySession=false;
95: /**
96: * @var array list of GET parameters that should participate in cache key calculation.
97: * By setting this property, the output cache will use different cached data
98: * for each different set of GET parameter values.
99: */
100: public $varyByParam;
101: /**
102: * @var string a PHP expression whose result is used in the cache key calculation.
103: * By setting this property, the output cache will use different cached data
104: * for each different expression result.
105: * The expression can also be a valid PHP callback,
106: * including class method name (array(ClassName/Object, MethodName)),
107: * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows:
108: * <pre>
109: * function foo($cache) { ... }
110: * </pre>
111: * where $cache refers to the output cache component.
112: *
113: * The PHP expression will be evaluated using {@link evaluateExpression}.
114: *
115: * A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
116: * please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
117: */
118: public $varyByExpression;
119: /**
120: * @var boolean whether the content being cached should be differentiated according to user's language.
121: * A language is retrieved via Yii::app()->language.
122: * Defaults to false.
123: * @since 1.1.14
124: */
125: public $varyByLanguage=false;
126: /**
127: * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only.
128: * Defaults to null, meaning all request types.
129: */
130: public $requestTypes;
131: /**
132: * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.)
133: */
134: public $cacheID='cache';
135: /**
136: * @var mixed the dependency that the cached content depends on.
137: * This can be either an object implementing {@link ICacheDependency} interface or an array
138: * specifying the configuration of the dependency object. For example,
139: * <pre>
140: * array(
141: * 'class'=>'CDbCacheDependency',
142: * 'sql'=>'SELECT MAX(lastModified) FROM Post',
143: * )
144: * </pre>
145: * would make the output cache depends on the last modified time of all posts.
146: * If any post has its modification time changed, the cached content would be invalidated.
147: */
148: public $dependency;
149:
150: private $_key;
151: private $_cache;
152: private $_contentCached;
153: private $_content;
154: private $_actions;
155:
156: /**
157: * Performs filtering before the action is executed.
158: * This method is meant to be overridden by child classes if begin-filtering is needed.
159: * @param CFilterChain $filterChain list of filters being applied to an action
160: * @return boolean whether the filtering process should stop after this filter. Defaults to false.
161: */
162: public function filter($filterChain)
163: {
164: if(!$this->getIsContentCached())
165: $filterChain->run();
166: $this->run();
167: }
168:
169: /**
170: * Marks the start of content to be cached.
171: * Content displayed after this method call and before {@link endCache()}
172: * will be captured and saved in cache.
173: * This method does nothing if valid content is already found in cache.
174: */
175: public function init()
176: {
177: if($this->getIsContentCached())
178: $this->replayActions();
179: elseif($this->_cache!==null)
180: {
181: $this->getController()->getCachingStack()->push($this);
182: ob_start();
183: ob_implicit_flush(false);
184: }
185: }
186:
187: /**
188: * Marks the end of content to be cached.
189: * Content displayed before this method call and after {@link init()}
190: * will be captured and saved in cache.
191: * This method does nothing if valid content is already found in cache.
192: */
193: public function run()
194: {
195: if($this->getIsContentCached())
196: {
197: if($this->getController()->isCachingStackEmpty())
198: echo $this->getController()->processDynamicOutput($this->_content);
199: else
200: echo $this->_content;
201: }
202: elseif($this->_cache!==null)
203: {
204: $this->_content=ob_get_clean();
205: $this->getController()->getCachingStack()->pop();
206: $data=array($this->_content,$this->_actions);
207: if(is_array($this->dependency))
208: $this->dependency=Yii::createComponent($this->dependency);
209: $this->_cache->set($this->getCacheKey(),$data,$this->duration,$this->dependency);
210:
211: if($this->getController()->isCachingStackEmpty())
212: echo $this->getController()->processDynamicOutput($this->_content);
213: else
214: echo $this->_content;
215: }
216: }
217:
218: /**
219: * @return boolean whether the content can be found from cache
220: */
221: public function getIsContentCached()
222: {
223: if($this->_contentCached!==null)
224: return $this->_contentCached;
225: else
226: return $this->_contentCached=$this->checkContentCache();
227: }
228:
229: /**
230: * Looks for content in cache.
231: * @return boolean whether the content is found in cache.
232: */
233: protected function checkContentCache()
234: {
235: if((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(),$this->requestTypes))
236: && ($this->_cache=$this->getCache())!==null)
237: {
238: if($this->duration>0 && ($data=$this->_cache->get($this->getCacheKey()))!==false)
239: {
240: $this->_content=$data[0];
241: $this->_actions=$data[1];
242: return true;
243: }
244: if($this->duration==0)
245: $this->_cache->delete($this->getCacheKey());
246: if($this->duration<=0)
247: $this->_cache=null;
248: }
249: return false;
250: }
251:
252: /**
253: * @return ICache the cache used for caching the content.
254: */
255: protected function getCache()
256: {
257: return Yii::app()->getComponent($this->cacheID);
258: }
259:
260: /**
261: * Caclulates the base cache key.
262: * The calculated key will be further variated in {@link getCacheKey}.
263: * Derived classes may override this method if more variations are needed.
264: * @return string basic cache key without variations
265: */
266: protected function getBaseCacheKey()
267: {
268: return self::CACHE_KEY_PREFIX.$this->getId().'.';
269: }
270:
271: /**
272: * Calculates the cache key.
273: * The key is calculated based on {@link getBaseCacheKey} and other factors, including
274: * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}.
275: * @return string cache key
276: */
277: protected function getCacheKey()
278: {
279: if($this->_key!==null)
280: return $this->_key;
281: else
282: {
283: $key=$this->getBaseCacheKey().'.';
284: if($this->varyByRoute)
285: {
286: $controller=$this->getController();
287: $key.=$controller->getUniqueId().'/';
288: if(($action=$controller->getAction())!==null)
289: $key.=$action->getId();
290: }
291: $key.='.';
292:
293: if($this->varyBySession)
294: $key.=Yii::app()->getSession()->getSessionID();
295: $key.='.';
296:
297: if(is_array($this->varyByParam) && isset($this->varyByParam[0]))
298: {
299: $params=array();
300: foreach($this->varyByParam as $name)
301: {
302: if(isset($_GET[$name]))
303: $params[$name]=$_GET[$name];
304: else
305: $params[$name]='';
306: }
307: $key.=serialize($params);
308: }
309: $key.='.';
310:
311: if($this->varyByExpression!==null)
312: $key.=$this->evaluateExpression($this->varyByExpression);
313: $key.='.';
314:
315: if($this->varyByLanguage)
316: $key.=Yii::app()->language;
317: $key.='.';
318:
319: return $this->_key=$key;
320: }
321: }
322:
323: /**
324: * Records a method call when this output cache is in effect.
325: * When the content is served from the output cache, the recorded
326: * method will be re-invoked.
327: * @param string $context a property name of the controller. The property should refer to an object
328: * whose method is being recorded. If empty it means the controller itself.
329: * @param string $method the method name
330: * @param array $params parameters passed to the method
331: */
332: public function recordAction($context,$method,$params)
333: {
334: $this->_actions[]=array($context,$method,$params);
335: }
336:
337: /**
338: * Replays the recorded method calls.
339: */
340: protected function replayActions()
341: {
342: if(empty($this->_actions))
343: return;
344: $controller=$this->getController();
345: $cs=Yii::app()->getClientScript();
346: foreach($this->_actions as $action)
347: {
348: if($action[0]==='clientScript')
349: $object=$cs;
350: elseif($action[0]==='')
351: $object=$controller;
352: else
353: $object=$controller->{$action[0]};
354: if(method_exists($object,$action[1]))
355: call_user_func_array(array($object,$action[1]),$action[2]);
356: elseif($action[0]==='' && function_exists($action[1]))
357: call_user_func_array($action[1],$action[2]);
358: else
359: throw new CException(Yii::t('yii','Unable to replay the action "{object}.{method}". The method does not exist.',
360: array('object'=>$action[0],
361: 'method'=>$action[1])));
362: }
363: }
364: }
365: