<?php
/*
* This file is part of the symfony package.
* (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Cache class to cache the HTML results for actions and templates.
*
* This class uses a sfCache instance implementation to store cache.
*
* To disable all caching, you can set the [sf_cache] constant to false.
*
* @package symfony
* @subpackage view
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @version SVN: $Id: sfViewCacheManager.class.php 24513 2009-11-28 23:34:54Z FabianLange $
*/
class sfViewCacheManager
{
protected
$cache = null,
$cacheConfig = array(),
$context = null,
$dispatcher = null,
$controller = null,
$routing = null,
$loaded = array();
/**
* Class constructor.
*
* @see initialize()
*/
public function __construct($context, sfCache $cache)
{
$this->initialize($context, $cache);
}
/**
* Initializes the cache manager.
*
* @param sfContext $context Current application context
* @param sfCache $cache An sfCache instance
*/
public function initialize($context, sfCache $cache)
{
$this->context = $context;
$this->dispatcher = $context->getEventDispatcher();
$this->controller = $context->getController();
if (sfConfig::get('sf_web_debug'))
{
$this->dispatcher->connect('view.cache.filter_content', array($this, 'decorateContentWithDebug'));
}
// empty configuration
$this->cacheConfig = array();
// cache instance
$this->cache = $cache;
// routing instance
$this->routing = $context->getRouting();
}
/**
* Retrieves the current cache context.
*
* @return sfContext The sfContext instance
*/
public function getContext()
{
return $this->context;
}
/**
* Retrieves the current cache object.
*
* @return sfCache The current cache object
*/
public function getCache()
{
return $this->cache;
}
/**
* Generates a unique cache key for an internal URI.
* This cache key can be used by any of the cache engines as a unique identifier to a cached resource
*
* Basically, the cache key generated for the following internal URI:
* module/action?key1=value1&key2=value2
* Looks like:
* /localhost/all/module/action/key1/value1/key2/value2
*
* @param string $internalUri The internal unified resource identifier
* Accepts rules formatted like 'module/action?key1=value1&key2=value2'
* Does not accept rules starting with a route name, except for '@sf_cache_partial'
* @param string $hostName The host name
* Optional - defaults to the current host name bu default
* @param string $vary The vary headers, separated by |, or "all" for all vary headers
* Defaults to 'all'
* @param string $contextualPrefix The contextual prefix for contextual partials.
* Defaults to 'currentModule/currentAction/currentPAram1/currentvalue1'
* Used only by the sfViewCacheManager::remove() method
*
* @return string The cache key
* If some of the parameters contained wildcards (* or **), the generated key will also have wildcards
*/
public function generateCacheKey($internalUri, $hostName = '', $vary = '', $contextualPrefix = '')
{
if ($callable = sfConfig::get('sf_cache_namespace_callable'))
{
if (!is_callable($callable))
{
throw new sfException(sprintf('"%s" cannot be called as a function.', var_export($callable, true)));
}
return call_user_func($callable, $internalUri, $hostName, $vary, $contextualPrefix, $this);
}
if (strpos($internalUri, '@') === 0 && strpos($internalUri, '@sf_cache_partial') === false)
{
throw new sfException('A cache key cannot be generated for an internal URI using the @rule syntax');
}
$cacheKey = '';
if ($this->isContextual($internalUri))
{
// Contextual partial
if (!$contextualPrefix)
{
list($route_name, $params) = $this->controller->convertUrlStringToParameters($this->routing->getCurrentInternalUri());
$cacheKey = $this->convertParametersToKey($params);
}
else
{
$cacheKey = $contextualPrefix;
}
list($route_name, $params) = $this->controller->convertUrlStringToParameters($internalUri);
$cacheKey .= sprintf('/%s/%s/%s', $params['module'], $params['action'], isset($params['sf_cache_key']) ? $params['sf_cache_key'] : '');
}
else
{
// Regular action or non-contextual partial
list($route_name, $params) = $this->controller->convertUrlStringToParameters($internalUri);
if ($route_name == 'sf_cache_partial')
{
$cacheKey = 'sf_cache_partial/';
}
$cacheKey .= $this->convertParametersToKey($params);
}
// prefix with vary headers
if (!$vary)
{
$varyHeaders = $this->getVary($internalUri);
if ($varyHeaders)
{
sort($varyHeaders);
$request = $this->context->getRequest();
$vary = '';
foreach ($varyHeaders as $header)
{
$vary .= $header . '_' . $request->getHttpHeader($header) . '_';
}
$vary = preg_replace('/[^a-z0-9]/i', '_', $vary);
$vary = preg_replace('/_+/', '_', $vary);
}
else
{
$vary = 'all';
}
}
// prefix with hostname
if (!$hostName)
{
$request = $this->context->getRequest();
$hostName = $request->getHost();
}
$hostName = preg_replace('/[^a-z0-9\*]/i', '_', $hostName);
$hostName = strtolower(preg_replace('/_+/', '_', $hostName));
$cacheKey = sprintf('/%s/%s/%s', $hostName, $vary, $cacheKey);
// replace multiple /
$cacheKey = preg_replace('#/+#', '/', $cacheKey);
return $cacheKey;
}
/**
* Transforms an associative array of parameters from an URI into a unique key
*
* @param array $params Associative array of parameters from the URI (including, at least, module and action)
*
* @return string Unique key
*/
protected function convertParametersToKey($params)
{
if(!isset($params['module']) || !isset($params['action']))
{
throw new sfException('A cache key must contain both a module and an action parameter');
}
$module = $params['module'];
unset($params['module']);
$action = $params['action'];
unset($params['action']);
ksort($params);
$cacheKey = sprintf('%s/%s', $module, $action);
foreach ($params as $key => $value)
{
$cacheKey .= sprintf('/%s/%s', $key, $value);
}
return $cacheKey;
}
/**
* Adds a cache to the manager.
*
* @param string $moduleName Module name
* @param string $actionName Action name
* @param array $options Options for the cache
*/
public function addCache($moduleName, $actionName, $options = array())
{
// normalize vary headers
if (isset($options['vary']))
{
foreach ($options['vary'] as $key => $name)
{
$options['vary'][$key] = strtr(strtolower($name), '_', '-');
}
}
$options['lifeTime'] = isset($options['lifeTime']) ? $options['lifeTime'] : 0;
if (!isset($this->cacheConfig[$moduleName]))
{
$this->cacheConfig[$moduleName] = array();
}
$this->cacheConfig[$moduleName][$actionName] = array(
'withLayout' => isset($options['withLayout']) ? $options['withLayout'] : false,
'lifeTime' => $options['lifeTime'],
'clientLifeTime' => isset($options['clientLifeTime']) ? $options['clientLifeTime'] : $options['lifeTime'],
'contextual' => isset($options['contextual']) ? $options['contextual'] : false,
'vary' => isset($options['vary']) ? $options['vary'] : array(),
);
}
/**
* Registers configuration options for the cache.
*
* @param string $moduleName Module name
*/
public function registerConfiguration($moduleName)
{
if (!isset($this->loaded[$moduleName]))
{
require($this->context->getConfigCache()->checkConfig('modules/'.$moduleName.'/config/cache.yml'));
$this->loaded[$moduleName] = true;
}
}
/**
* Retrieves the layout from the cache option list.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return bool true, if have layout otherwise false
*/
public function withLayout($internalUri)
{
return $this->getCacheConfig($internalUri, 'withLayout', false);
}
/**
* Retrieves lifetime from the cache option list.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return int LifeTime
*/
public function getLifeTime($internalUri)
{
return $this->getCacheConfig($internalUri, 'lifeTime', 0);
}
/**
* Retrieves client lifetime from the cache option list
*
* @param string $internalUri Internal uniform resource identifier
*
* @return int Client lifetime
*/
public function getClientLifeTime($internalUri)
{
return $this->getCacheConfig($internalUri, 'clientLifeTime', 0);
}
/**
* Retrieves contextual option from the cache option list.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return boolean true, if is contextual otherwise false
*/
public function isContextual($internalUri)
{
return $this->getCacheConfig($internalUri, 'contextual', false);
}
/**
* Retrieves vary option from the cache option list.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return array Vary options for the cache
*/
public function getVary($internalUri)
{
return $this->getCacheConfig($internalUri, 'vary', array());
}
/**
* Gets a config option from the cache.
*
* @param string $internalUri Internal uniform resource identifier
* @param string $key Option name
* @param string $defaultValue Default value of the option
*
* @return mixed Value of the option
*/
protected function getCacheConfig($internalUri, $key, $defaultValue = null)
{
list($route_name, $params) = $this->controller->convertUrlStringToParameters($internalUri);
$this->registerConfiguration($params['module']);
$value = $defaultValue;
if (isset($this->cacheConfig[$params['module']][$params['action']][$key]))
{
$value = $this->cacheConfig[$params['module']][$params['action']][$key];
}
else if (isset($this->cacheConfig[$params['module']]['DEFAULT'][$key]))
{
$value = $this->cacheConfig[$params['module']]['DEFAULT'][$key];
}
return $value;
}
/**
* Returns true if the current content is cacheable.
*
* Possible break in backward compatibility: If the sf_lazy_cache_key
* setting is turned on in settings.yml, this method is not used when
* initially checking a partial's cacheability.
*
* @see sfPartialView, isActionCacheable()
*
* @param string $internalUri Internal uniform resource identifier
*
* @return bool true, if the content is cacheable otherwise false
*/
public function isCacheable($internalUri)
{
if (count($_GET) || count($_POST))
{
return false;
}
list($route_name, $params) = $this->controller->convertUrlStringToParameters($internalUri);
$this->registerConfiguration($params['module']);
if (isset($this->cacheConfig[$params['module']][$params['action']]))
{
return ($this->cacheConfig[$params['module']][$params['action']]['lifeTime'] > 0);
}
else if (isset($this->cacheConfig[$params['module']]['DEFAULT']))
{
return ($this->cacheConfig[$params['module']]['DEFAULT']['lifeTime'] > 0);
}
return false;
}
/**
* Returns true if the action is cacheable.
*
* @param string $moduleName A module name
* @param string $actionName An action or partial template name
*
* @return boolean True if the action is cacheable
*
* @see isCacheable()
*/
public function isActionCacheable($moduleName, $actionName)
{
if (count($_GET) || count($_POST))
{
return false;
}
$this->registerConfiguration($moduleName);
if (isset($this->cacheConfig[$moduleName][$actionName]))
{
return $this->cacheConfig[$moduleName][$actionName]['lifeTime'] > 0;
}
else if (isset($this->cacheConfig[$moduleName]['DEFAULT']))
{
return $this->cacheConfig[$moduleName]['DEFAULT']['lifeTime'] > 0;
}
return false;
}
/**
* Retrieves content in the cache.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return string The content in the cache
*/
public function get($internalUri)
{
// no cache or no cache set for this action
if (!$this->isCacheable($internalUri) || $this->ignore())
{
return null;
}
$retval = $this->cache->get($this->generateCacheKey($internalUri));
if (sfConfig::get('sf_logging_enabled'))
{
$this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Cache for "%s" %s', $internalUri, $retval !== null ? 'exists' : 'does not exist'))));
}
return $retval;
}
/**
* Returns true if there is a cache.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return bool true, if there is a cache otherwise false
*/
public function has($internalUri)
{
if (!$this->isCacheable($internalUri) || $this->ignore())
{
return null;
}
return $this->cache->has($this->generateCacheKey($internalUri));
}
/**
* Ignores the cache functionality.
*
* @return bool true, if the cache is ignore otherwise false
*/
protected function ignore()
{
// ignore cache parameter? (only available in debug mode)
if (sfConfig::get('sf_debug') && $this->context->getRequest()->getAttribute('sf_ignore_cache'))
{
if (sfConfig::get('sf_logging_enabled'))
{
$this->dispatcher->notify(new sfEvent($this, 'application.log', array('Discard cache')));
}
return true;
}
return false;
}
/**
* Sets the cache content.
*
* @param string $data Data to put in the cache
* @param string $internalUri Internal uniform resource identifier
*
* @return boolean true, if the data get set successfully otherwise false
*/
public function set($data, $internalUri)
{
if (!$this->isCacheable($internalUri))
{
return false;
}
try
{
$ret = $this->cache->set($this->generateCacheKey($internalUri), $data, $this->getLifeTime($internalUri));
}
catch (Exception $e)
{
return false;
}
if (sfConfig::get('sf_logging_enabled'))
{
$this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Save cache for "%s"', $internalUri))));
}
return true;
}
/**
* Removes the content in the cache.
*
* @param string $internalUri Internal uniform resource identifier
* @param string $hostName The host name
* @param string $vary The vary headers, separated by |, or "all" for all vary headers
* @param string $contextualPrefix The removal prefix for contextual partials. Defaults to '**' (all actions, all params)
*
* @return bool true, if the remove happened, false otherwise
*/
public function remove($internalUri, $hostName = '', $vary = '', $contextualPrefix = '**')
{
if (sfConfig::get('sf_logging_enabled'))
{
$this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Remove cache for "%s"', $internalUri))));
}
$cacheKey = $this->generateCacheKey($internalUri, $hostName, $vary, $contextualPrefix);
if(strpos($cacheKey, '*'))
{
return $this->cache->removePattern($cacheKey);
}
elseif ($this->cache->has($cacheKey))
{
return $this->cache->remove($cacheKey);
}
}
/**
* Retrieves the last modified time.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return int The last modified datetime
*/
public function getLastModified($internalUri)
{
if (!$this->isCacheable($internalUri))
{
return 0;
}
return $this->cache->getLastModified($this->generateCacheKey($internalUri));
}
/**
* Retrieves the timeout.
*
* @param string $internalUri Internal uniform resource identifier
*
* @return int The timeout datetime
*/
public function getTimeout($internalUri)
{
if (!$this->isCacheable($internalUri))
{
return 0;
}
return $this->cache->getTimeout($this->generateCacheKey($internalUri));
}
/**
* Starts the fragment cache.
*
* @param string $name Unique fragment name
* @param string $lifeTime Life time for the cache
* @param string $clientLifeTime Client life time for the cache
* @param array $vary Vary options for the cache
*
* @return bool true, if success otherwise false
*/
public function start($name, $lifeTime, $clientLifeTime = null, $vary = array())
{
$internalUri = $this->routing->getCurrentInternalUri();
if (!$clientLifeTime)
{
$clientLifeTime = $lifeTime;
}
// add cache config to cache manager
list($route_name, $params) = $this->controller->convertUrlStringToParameters($internalUri);
$this->addCache($params['module'], $params['action'], array('withLayout' => false, 'lifeTime' => $lifeTime, 'clientLifeTime' => $clientLifeTime, 'vary' => $vary));
// get data from cache if available
$data = $this->get($internalUri.(strpos($internalUri, '?') ? '&' : '?').'_sf_cache_key='.$name);
if ($data !== null)
{
return $data;
}
else
{
ob_start();
ob_implicit_flush(0);
return null;
}
}
/**
* Stops the fragment cache.
*
* @param string $name Unique fragment name
*
* @return bool true, if success otherwise false
*/
public function stop($name)
{
$data = ob_get_clean();
// save content to cache
$internalUri = $this->routing->getCurrentInternalUri();
try
{
$this->set($data, $internalUri.(strpos($internalUri, '?') ? '&' : '?').'_sf_cache_key='.$name);
}
catch (Exception $e)
{
}
return $data;
}
/**
* Computes the cache key based on the passed parameters.
*
* @param array $parameters An array of parameters
*/
public function computeCacheKey(array $parameters)
{
if (isset($parameters['sf_cache_key']))
{
return $parameters['sf_cache_key'];
}
if (sfConfig::get('sf_logging_enabled'))
{
$this->dispatcher->notify(new sfEvent($this, 'application.log', array('Generate cache key')));
}
return md5(serialize($parameters));
}
/**
* Checks that the supplied parameters include a cache key.
*
* If no 'sf_cache_key' parameter is present one is added to the array as
* it is passed by reference.
*
* @param array $parameters An array of parameters
*
* @return string The cache key
*/
public function checkCacheKey(array & $parameters)
{
$parameters['sf_cache_key'] = $this->computeCacheKey($parameters);
return $parameters['sf_cache_key'];
}
/**
* Computes a partial internal URI.
*
* @param string $module The module name
* @param string $action The action name
* @param string $cacheKey The cache key
*
* @return string The internal URI
*/
public function getPartialUri($module, $action, $cacheKey)
{
return sprintf('@sf_cache_partial?module=%s&action=%s&sf_cache_key=%s', $module, $action, $cacheKey);
}
/**
* Returns whether a partial template is in the cache.
*
* @param string $module The module name
* @param string $action The action name
* @param string $cacheKey The cache key
*
* @return bool true if a partial is in the cache, false otherwise
*/
public function hasPartialCache($module, $action, $cacheKey)
{
return $this->has($this->getPartialUri($module, $action, $cacheKey));
}
/**
* Gets a partial template from the cache.
*
* @param string $module The module name
* @param string $action The action name
* @param string $cacheKey The cache key
*
* @return string The cache content
*/
public function getPartialCache($module, $action, $cacheKey)
{
$uri = $this->getPartialUri($module, $action, $cacheKey);
if (!$this->isCacheable($uri))
{
return null;
}
// retrieve content from cache
$cache = $this->get($uri);
if (is_null($cache))
{
return null;
}
$cache = unserialize($cache);
$content = $cache['content'];
$this->context->getResponse()->merge($cache['response']);
if (sfConfig::get('sf_web_debug'))
{
$content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array('response' => $this->context->getResponse(), 'uri' => $uri, 'new' => false)), $content)->getReturnValue();
}
return $content;
}
/**
* Sets an action template in the cache.
*
* @param string $module The module name
* @param string $action The action name
* @param string $cacheKey The cache key
* @param string $content The content to cache
*
* @return string The cached content
*/
public function setPartialCache($module, $action, $cacheKey, $content)
{
$uri = $this->getPartialUri($module, $action, $cacheKey);
if (!$this->isCacheable($uri))
{
return $content;
}
$saved = $this->set(serialize(array('content' => $content, 'response' => $this->context->getResponse())), $uri);
if ($saved && sfConfig::get('sf_web_debug'))
{
$content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array('response' => $this->context->getResponse(), 'uri' => $uri, 'new' => true)), $content)->getReturnValue();
}
return $content;
}
/**
* Returns whether an action template is in the cache.
*
* @param string $uri The internal URI
*
* @return bool true if an action is in the cache, false otherwise
*/
public function hasActionCache($uri)
{
return $this->has($uri) && !$this->withLayout($uri);
}
/**
* Gets an action template from the cache.
*
* @param string $uri The internal URI
*
* @return array An array composed of the cached content and the view attribute holder
*/
public function getActionCache($uri)
{
if (!$this->isCacheable($uri) || $this->withLayout($uri))
{
return null;
}
// retrieve content from cache
$cache = $this->get($uri);
if (is_null($cache))
{
return null;
}
$cache = unserialize($cache);
$content = $cache['content'];
$cache['response']->setEventDispatcher($this->dispatcher);
$this->context->getResponse()->copyProperties($cache['response']);
if (sfConfig::get('sf_web_debug'))
{
$content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array('response' => $this->context->getResponse(), 'uri' => $uri, 'new' => false)), $content)->getReturnValue();
}
return array($content, $cache['decoratorTemplate']);
}
/**
* Sets an action template in the cache.
*
* @param string $uri The internal URI
* @param string $content The content to cache
* @param string $decoratorTemplate The view attribute holder to cache
*
* @return string The cached content
*/
public function setActionCache($uri, $content, $decoratorTemplate)
{
if (!$this->isCacheable($uri) || $this->withLayout($uri))
{
return $content;
}
$saved = $this->set(serialize(array('content' => $content, 'decoratorTemplate' => $decoratorTemplate, 'response' => $this->context->getResponse())), $uri);
if ($saved && sfConfig::get('sf_web_debug'))
{
$content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array('response' => $this->context->getResponse(), 'uri' => $uri, 'new' => true)), $content)->getReturnValue();
}
return $content;
}
/**
* Sets a page in the cache.
*
* @param string $uri The internal URI
*/
public function setPageCache($uri)
{
if (sfView::RENDER_CLIENT != $this->controller->getRenderMode())
{
return;
}
// save content in cache
$saved = $this->set(serialize($this->context->getResponse()), $uri);
if ($saved && sfConfig::get('sf_web_debug'))
{
$content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array('response' => $this->context->getResponse(), 'uri' => $uri, 'new' => true)), $this->context->getResponse()->getContent())->getReturnValue();
$this->context->getResponse()->setContent($content);
}
}
/**
* Gets a page from the cache.
*
* @param string $uri The internal URI
*
* @return string The cached page
*/
public function getPageCache($uri)
{
$retval = $this->get($uri);
if (is_null($retval))
{
return false;
}
$cachedResponse = unserialize($retval);
$cachedResponse->setEventDispatcher($this->dispatcher);
if (sfView::RENDER_VAR == $this->controller->getRenderMode())
{
$this->controller->getActionStack()->getLastEntry()->setPresentation($cachedResponse->getContent());
$this->context->getResponse()->setContent('');
}
else
{
$this->context->setResponse($cachedResponse);
if (sfConfig::get('sf_web_debug'))
{
$content = $this->dispatcher->filter(new sfEvent($this, 'view.cache.filter_content', array('response' => $this->context->getResponse(), 'uri' => $uri, 'new' => false)), $this->context->getResponse()->getContent())->getReturnValue();
$this->context->getResponse()->setContent($content);
}
}
return true;
}
/**
* Listens to the 'view.cache.filter_content' event to decorate a chunk of HTML with cache information.
*
* @param sfEvent $event A sfEvent instance
* @param string $content The HTML content
*
* @return string The decorated HTML string
*/
public function decorateContentWithDebug(sfEvent $event, $content)
{
// don't decorate if not html or if content is null
if (!$content || false === strpos($event['response']->getContentType(), 'html'))
{
return $content;
}
$this->context->getConfiguration()->loadHelpers(array('Helper', 'Url', 'Asset', 'Tag'));
$bgColor = $event['new'] ? '#9ff' : '#ff9';
$lastModified = $this->getLastModified($event['uri']);
$id = md5($event['uri']);
return '
<div id="main_'.$id.'" class="sfWebDebugActionCache" style="border: 1px solid #f00">
<div id="sub_main_'.$id.'" class="sfWebDebugCache" style="background-color: '.$bgColor.'; border-right: 1px solid #f00; border-bottom: 1px solid #f00;">
<div style="height: 16px; padding: 2px"><a href="#" onclick="sfWebDebugToggle(\'sub_main_info_'.$id.'\'); return false;"><strong>cache information</strong></a> <a href="#" onclick="sfWebDebugToggle(\'sub_main_'.$id.'\'); document.getElementById(\'main_'.$id.'\').style.border = \'none\'; return false;">'.image_tag(sfConfig::get('sf_web_debug_web_dir').'/images/close.png', array('alt' => 'close')).'</a> </div>
<div style="padding: 2px; display: none" id="sub_main_info_'.$id.'">
[uri] '.htmlspecialchars($event['uri'], ENT_QUOTES, sfConfig::get('sf_charset')).'<br />
[life time] '.$this->getLifeTime($event['uri']).' seconds<br />
[last modified] '.(time() - $lastModified).' seconds<br />
<br />
</div>
</div><div>
'.$content.'
</div></div>
';
}
}