diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php index 2655c5a..1f82e5d 100644 --- a/framework/base/ActionFilter.php +++ b/framework/base/ActionFilter.php @@ -11,17 +11,27 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -class Filter extends Behavior +class ActionFilter extends Behavior { /** + * @var array list of action IDs that this filter should apply to. If this property is not set, + * then the filter applies to all actions, unless they are listed in [[except]]. + */ + public $only; + /** + * @var array list of action IDs that this filter should not apply to. + */ + public $except = array(); + + /** * Declares event handlers for the [[owner]]'s events. * @return array events (array keys) and the corresponding event handler methods (array values). */ public function events() { return array( - 'beforeAction' => 'beforeAction', - 'afterAction' => 'afterAction', + 'beforeAction' => 'beforeFilter', + 'afterAction' => 'afterFilter', ); } @@ -29,8 +39,11 @@ class Filter extends Behavior * @param ActionEvent $event * @return boolean */ - public function beforeAction($event) + public function beforeFilter($event) { + if ($this->isActive($event->action)) { + $event->isValid = $this->beforeAction($event->action); + } return $event->isValid; } @@ -38,8 +51,40 @@ class Filter extends Behavior * @param ActionEvent $event * @return boolean */ - public function afterAction($event) + public function afterFilter($event) { + if ($this->isActive($event->action)) { + $this->afterAction($event->action); + } + } + /** + * This method is invoked right before an action is to be executed (after all possible filters.) + * You may override this method to do last-minute preparation for the action. + * @param Action $action the action to be executed. + * @return boolean whether the action should continue to be executed. + */ + public function beforeAction($action) + { + return true; + } + + /** + * This method is invoked right after an action is executed. + * You may override this method to do some postprocessing for the action. + * @param Action $action the action just executed. + */ + public function afterAction($action) + { + } + + /** + * Returns a value indicating whether the filer is active for the given action. + * @param Action $action the action being filtered + * @return boolean whether the filer is active for the given action. + */ + protected function isActive($action) + { + return !in_array($action->id, $this->except, true) && (empty($this->only) || in_array($action->id, $this->only, true)); } } \ No newline at end of file diff --git a/framework/base/View.php b/framework/base/View.php index baa1d10..18748f2 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -337,15 +337,16 @@ class View extends Component * ~~~ * * @param string $id a unique ID identifying the fragment to be cached. - * @param array $properties initial property values for [[\yii\widgets\OutputCache]] + * @param array $properties initial property values for [[\yii\widgets\FragmentCache]] * @return boolean whether you should generate the content for caching. * False if the cached version is available. */ public function beginCache($id, $properties = array()) { $properties['id'] = $id; + /** @var $cache \yii\widgets\FragmentCache */ $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); - if ($cache->getIsContentCached()) { + if ($cache->getCachedContent() !== false) { $this->endCache(); return false; } else { diff --git a/framework/caching/Cache.php b/framework/caching/Cache.php index f3f1bc0..70cf8cb 100644 --- a/framework/caching/Cache.php +++ b/framework/caching/Cache.php @@ -8,6 +8,7 @@ namespace yii\caching; use yii\base\Component; +use yii\helpers\StringHelper; /** * Cache is the base class for cache classes supporting different cache storage implementation. @@ -70,13 +71,13 @@ abstract class Cache extends Component implements \ArrayAccess /** - * Builds a normalized cache key from one or multiple parameters. + * Builds a normalized cache key from a given key. * * The generated key contains letters and digits only, and its length is no more than 32. * - * If only one parameter is given and it is already a normalized key, then - * it will be returned back without change. Otherwise, a normalized key - * is generated by serializing all given parameters and applying MD5 hashing. + * If the given key is a string containing alphanumeric characters only and no more than 32 characters, + * then the key will be returned back without change. Otherwise, a normalized key + * is generated by serializing the given key and applying MD5 hashing. * * The following example builds a cache key using three parameters: * @@ -84,16 +85,15 @@ abstract class Cache extends Component implements \ArrayAccess * $key = $cache->buildKey($className, $method, $id); * ~~~ * - * @param string $key the first parameter + * @param array|string $key the key to be normalized * @return string the generated cache key */ public function buildKey($key) { - if (func_num_args() === 1 && ctype_alnum($key) && strlen($key) <= 32) { - return (string)$key; + if (is_string($key)) { + return ctype_alnum($key) && StringHelper::strlen($key) <= 32 ? $key : md5($key); } else { - $params = func_get_args(); - return md5(json_encode($params)); + return md5(json_encode($key)); } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index 9c6e1f1..247109b 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -7,15 +7,15 @@ namespace yii\caching; +use Yii; use yii\base\InvalidConfigException; use yii\db\Connection; -use yii\db\Query; /** * DbDependency represents a dependency based on the query result of a SQL statement. * * If the query result changes, the dependency is considered as changed. - * The query is specified via the [[query]] property. + * The query is specified via the [[sql]] property. * * @author Qiang Xue * @since 2.0 @@ -27,23 +27,25 @@ class DbDependency extends Dependency */ public $connectionID = 'db'; /** - * @var Query the SQL query whose result is used to determine if the dependency has been changed. + * @var string the SQL query whose result is used to determine if the dependency has been changed. * Only the first row of the query result will be used. */ - public $query; + public $sql; /** - * @var Connection the DB connection instance + * @var array the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. */ - private $_db; + public $params; /** * Constructor. - * @param Query $query the SQL query whose result is used to determine if the dependency has been changed. + * @param string $sql the SQL query whose result is used to determine if the dependency has been changed. + * @param array $params the parameters (name=>value) to be bound to the SQL statement specified by [[sql]]. * @param array $config name-value pairs that will be used to initialize the object properties */ - public function __construct($query = null, $config = array()) + public function __construct($sql, $params = array(), $config = array()) { - $this->query = $query; + $this->sql = $sql; + $this->params = $params; parent::__construct($config); } @@ -66,22 +68,23 @@ class DbDependency extends Dependency protected function generateDependencyData() { $db = $this->getDb(); - /** - * @var \yii\db\Command $command - */ - $command = $this->query->createCommand($db); if ($db->enableQueryCache) { // temporarily disable and re-enable query caching $db->enableQueryCache = false; - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); $db->enableQueryCache = true; } else { - $result = $command->queryRow(); + $result = $db->createCommand($this->sql, $this->params)->queryRow(); } return $result; } /** + * @var Connection the DB connection instance + */ + private $_db; + + /** * Returns the DB connection instance used for caching purpose. * @return Connection the DB connection instance * @throws InvalidConfigException if [[connectionID]] does not point to a valid application component. @@ -89,11 +92,11 @@ class DbDependency extends Dependency public function getDb() { if ($this->_db === null) { - $db = \Yii::$app->getComponent($this->connectionID); + $db = Yii::$app->getComponent($this->connectionID); if ($db instanceof Connection) { $this->_db = $db; } else { - throw new InvalidConfigException("DbCache::connectionID must refer to the ID of a DB application component."); + throw new InvalidConfigException("DbCacheDependency::connectionID must refer to the ID of a DB application component."); } } return $this->_db; diff --git a/framework/db/Command.php b/framework/db/Command.php index 0861b8d..c2b2e05 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -389,7 +389,13 @@ class Command extends \yii\base\Component } if (isset($cache)) { - $cacheKey = $cache->buildKey(__CLASS__, $db->dsn, $db->username, $sql, $paramLog); + $cacheKey = $cache->buildKey(array( + __CLASS__, + $db->dsn, + $db->username, + $sql, + $paramLog, + )); if (($result = $cache->get($cacheKey)) !== false) { \Yii::trace('Query result found in cache', __CLASS__); return $result; diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 5a6ffda..5fe6121 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -109,7 +109,12 @@ abstract class Schema extends \yii\base\Object */ public function getCacheKey($cache, $name) { - return $cache->buildKey(__CLASS__, $this->db->dsn, $this->db->username, $name); + return $cache->buildKey(array( + __CLASS__, + $this->db->dsn, + $this->db->username, + $name, + )); } /** diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index c64db24..429aeb1 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -7,15 +7,7 @@ namespace yii\helpers; -// todo define how subclassing will work -// todo add a run() or render() method // todo test this on all kinds of terminals, especially windows (check out lib ncurses) -// todo not sure if all methods should be static - -// todo subclass DetailView -// todo subclass GridView -// todo more subclasses - /** * Console View is the base class for console view components diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index 25524a0..d7882a6 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -45,6 +45,7 @@ class CacheSession extends Session { return true; } + /** * Returns the cache instance used for storing session data. * @return Cache the cache instance @@ -114,6 +115,6 @@ class CacheSession extends Session */ protected function calculateKey($id) { - return $this->getCache()->buildKey(__CLASS__, $id); + return $this->getCache()->buildKey(array(__CLASS__, $id)); } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index e6805f5..38073f0 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -7,7 +7,11 @@ namespace yii\widgets; +use Yii; +use yii\base\InvalidConfigException; use yii\base\Widget; +use yii\caching\Cache; +use yii\caching\Dependency; /** * @author Qiang Xue @@ -16,81 +20,47 @@ use yii\base\Widget; class FragmentCache extends Widget { /** - * Prefix to the keys for storing cached data - */ - const CACHE_KEY_PREFIX = 'Yii.COutputCache.'; - - /** * @var string the ID of the cache application component. Defaults to 'cache' (the primary cache application component.) */ public $cacheID = 'cache'; /** - * @var integer number of seconds that the data can remain in cache. Defaults to 60 seconds. - * If it is 0, existing cached content would be removed from the cache. - * If it is a negative value, the cache will be disabled (any existing cached content will - * remain in the cache.) - * - * Note, if cache dependency changes or cache space is limited, - * the data may be purged out of cache earlier. + * @var integer number of seconds that the data can remain valid in cache. + * Use 0 to indicate that the cached data will never expire. */ public $duration = 60; /** - * @var mixed the dependency that the cached content depends on. - * This can be either an object implementing {@link ICacheDependency} interface or an array - * specifying the configuration of the dependency object. For example, - *
+	 * @var array|Dependency the dependency that the cached content depends on.
+	 * This can be either a [[Dependency]] object or a configuration array for creating the dependency object.
+	 * For example,
+	 *
+	 * ~~~
 	 * array(
-	 *     'class'=>'CDbCacheDependency',
-	 *     'sql'=>'SELECT MAX(lastModified) FROM Post',
+	 *     'class' => 'yii\caching\DbDependency',
+	 *     'sql' => 'SELECT MAX(lastModified) FROM Post',
 	 * )
-	 * 
+ * ~~~ + * * would make the output cache depends on the last modified time of all posts. * If any post has its modification time changed, the cached content would be invalidated. */ public $dependency; /** - * @var boolean whether the content being cached should be differentiated according to route. - * A route consists of the requested controller ID and action ID. - * Defaults to true. - */ - public $varyByRoute = true; - /** - * @var boolean whether the content being cached should be differentiated according to user's language. - * A language is retrieved via Yii::app()->language. - * Defaults to false. - * @since 1.1.14 - */ - public $varyByLanguage = false; - /** - * @var array list of GET parameters that should participate in cache key calculation. - * By setting this property, the output cache will use different cached data - * for each different set of GET parameter values. - */ - public $varyByParam; - /** - * @var string a PHP expression whose result is used in the cache key calculation. - * By setting this property, the output cache will use different cached data - * for each different expression result. - * The expression can also be a valid PHP callback, - * including class method name (array(ClassName/Object, MethodName)), - * or anonymous function (PHP 5.3.0+). The function/method signature should be as follows: - *
-	 * function foo($cache) { ... }
-	 * 
- * where $cache refers to the output cache component. + * @var array list of factors that would cause the variation of the content being cached. + * Each factor is a string representing a variation (e.g. the language, a GET parameter). + * The following variation setting will cause the content to be cached in different versions + * according to the current application language: + * + * ~~~ + * array( + * Yii::$app->language, + * ) */ - public $varyByExpression; + public $variations; /** - * @var array list of request types (e.g. GET, POST) for which the cache should be enabled only. - * Defaults to null, meaning all request types. + * @var boolean whether to enable the fragment cache. You may use this property to turn on and off + * the fragment cache according to specific setting (e.g. enable fragment cache only for GET requests). */ - public $requestTypes; - - private $_key; - private $_cache; - private $_contentCached; - private $_content; - private $_actions; + public $enabled = true; /** * Marks the start of content to be cached. @@ -100,10 +70,7 @@ class FragmentCache extends Widget */ public function init() { - if ($this->getIsContentCached()) { - $this->replayActions(); - } elseif ($this->_cache !== null) { - $this->getController()->getCachingStack()->push($this); + if ($this->getCachedContent() === false && $this->getCache() !== null) { ob_start(); ob_implicit_flush(false); } @@ -117,178 +84,89 @@ class FragmentCache extends Widget */ public function run() { - if ($this->getIsContentCached()) { - if ($this->getController()->isCachingStackEmpty()) { - echo $this->getController()->processDynamicOutput($this->_content); - } else { - echo $this->_content; - } - } elseif ($this->_cache !== null) { - $this->_content = ob_get_clean(); - $this->getController()->getCachingStack()->pop(); - $data = array($this->_content, $this->_actions); + if (($content = $this->getCachedContent()) !== false) { + echo $content; + } elseif (($cache = $this->getCache()) !== false) { + $content = ob_get_clean(); if (is_array($this->dependency)) { - $this->dependency = Yii::createComponent($this->dependency); - } - $this->_cache->set($this->getCacheKey(), $data, $this->duration, $this->dependency); - - if ($this->getController()->isCachingStackEmpty()) { - echo $this->getController()->processDynamicOutput($this->_content); - } else { - echo $this->_content; + $this->dependency = Yii::createObject($this->dependency); } + $cache->set($this->calculateKey(), $content, $this->duration, $this->dependency); + echo $content; } } /** - * @return boolean whether the content can be found from cache + * @var string|boolean the cached content. False if the content is not cached. */ - public function getIsContentCached() - { - if ($this->_contentCached !== null) { - return $this->_contentCached; - } else { - return $this->_contentCached = $this->checkContentCache(); - } - } + private $_content; /** - * Looks for content in cache. - * @return boolean whether the content is found in cache. + * Returns the cached content if available. + * @return string|boolean the cached content. False is returned if valid content is not found in the cache. */ - protected function checkContentCache() + public function getCachedContent() { - if ((empty($this->requestTypes) || in_array(Yii::app()->getRequest()->getRequestType(), $this->requestTypes)) - && ($this->_cache = $this->getCache()) !== null - ) { - if ($this->duration > 0 && ($data = $this->_cache->get($this->getCacheKey())) !== false) { - $this->_content = $data[0]; - $this->_actions = $data[1]; - return true; - } - if ($this->duration == 0) { - $this->_cache->delete($this->getCacheKey()); - } - if ($this->duration <= 0) { - $this->_cache = null; + if ($this->_content === null) { + if (($cache = $this->getCache()) !== null) { + $key = $this->calculateKey(); + $this->_content = $cache->get($key); + } else { + $this->_content = false; } } - return false; + return $this->_content; } /** - * @return ICache the cache used for caching the content. + * Generates a unique key used for storing the content in cache. + * The key generated depends on both [[id]] and [[variations]]. + * @return string a valid cache key */ - protected function getCache() + protected function calculateKey() { - return Yii::app()->getComponent($this->cacheID); + $factors = array(__CLASS__, $this->getId()); + if (is_array($this->variations)) { + foreach ($this->variations as $factor) { + $factors[] = $factor; + } + } + return $this->getCache()->buildKey($factors); } /** - * Caclulates the base cache key. - * The calculated key will be further variated in {@link getCacheKey}. - * Derived classes may override this method if more variations are needed. - * @return string basic cache key without variations + * @var Cache */ - protected function getBaseCacheKey() - { - return self::CACHE_KEY_PREFIX . $this->getId() . '.'; - } + private $_cache; /** - * Calculates the cache key. - * The key is calculated based on {@link getBaseCacheKey} and other factors, including - * {@link varyByRoute}, {@link varyByParam}, {@link varyBySession} and {@link varyByLanguage}. - * @return string cache key + * Returns the cache instance used for storing content. + * @return Cache the cache instance. Null is returned if the cache component is not available + * or [[enabled]] is false. + * @throws InvalidConfigException if [[cacheID]] does not point to a valid application component. */ - protected function getCacheKey() + public function getCache() { - if ($this->_key !== null) { - return $this->_key; - } else { - $key = $this->getBaseCacheKey() . '.'; - if ($this->varyByRoute) { - $controller = $this->getController(); - $key .= $controller->getUniqueId() . '/'; - if (($action = $controller->getAction()) !== null) { - $key .= $action->getId(); - } - } - $key .= '.'; - - if ($this->varyBySession) { - $key .= Yii::app()->getSession()->getSessionID(); - } - $key .= '.'; - - if (is_array($this->varyByParam) && isset($this->varyByParam[0])) { - $params = array(); - foreach ($this->varyByParam as $name) { - if (isset($_GET[$name])) { - $params[$name] = $_GET[$name]; - } else { - $params[$name] = ''; - } - } - $key .= serialize($params); - } - $key .= '.'; - - if ($this->varyByExpression !== null) { - $key .= $this->evaluateExpression($this->varyByExpression); - } - $key .= '.'; - - if ($this->varyByLanguage) { - $key .= Yii::app()->language; + if (!$this->enabled) { + return null; + } + if ($this->_cache === null) { + $cache = Yii::$app->getComponent($this->cacheID); + if ($cache instanceof Cache) { + $this->_cache = $cache; + } else { + throw new InvalidConfigException('FragmentCache::cacheID must refer to the ID of a cache application component.'); } - $key .= '.'; - - return $this->_key = $key; } + return $this->_cache; } /** - * Records a method call when this output cache is in effect. - * When the content is served from the output cache, the recorded - * method will be re-invoked. - * @param string $context a property name of the controller. The property should refer to an object - * whose method is being recorded. If empty it means the controller itself. - * @param string $method the method name - * @param array $params parameters passed to the method - */ - public function recordAction($context, $method, $params) - { - $this->_actions[] = array($context, $method, $params); - } - - /** - * Replays the recorded method calls. + * Sets the cache instance used by the session component. + * @param Cache $value the cache instance */ - protected function replayActions() + public function setCache($value) { - if (empty($this->_actions)) { - return; - } - $controller = $this->getController(); - $cs = Yii::app()->getClientScript(); - foreach ($this->_actions as $action) { - if ($action[0] === 'clientScript') { - $object = $cs; - } elseif ($action[0] === '') { - $object = $controller; - } else { - $object = $controller->{$action[0]}; - } - if (method_exists($object, $action[1])) { - call_user_func_array(array($object, $action[1]), $action[2]); - } elseif ($action[0] === '' && function_exists($action[1])) { - call_user_func_array($action[1], $action[2]); - } else { - throw new CException(Yii::t('yii', 'Unable to replay the action "{object}.{method}". The method does not exist.', - array('object' => $action[0], - 'method' => $action[1]))); - } - } + $this->_cache = $value; } } \ No newline at end of file diff --git a/todo.md b/todo.md index f102f41..f66d3c1 100644 --- a/todo.md +++ b/todo.md @@ -1,21 +1,4 @@ -- console - * If console is executed using Windows, do not use colors. If not, use colors. Allow to override via console application settings. -- db - * pgsql, sql server, oracle, db2 drivers - * unit tests on different DB drivers - * document-based (should allow storage-specific methods additionally to generic ones) - * mongodb (put it under framework/db/mongodb) - * key-value-based (should allow storage-specific methods additionally to generic ones) - * redis (put it under framework/db/redis or perhaps framework/caching?) -- base - * TwigViewRenderer (Alex) - * SmartyViewRenderer (Alex) -- logging - * WebTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * ProfileTarget (TBD after web is in place): should consider using javascript and make it into a toolbar - * unit tests - caching - * backend-specific unit tests * dependency unit tests - validators * Refactor validators to add validateValue() for every validator, if possible. Check if value is an array.