diff --git a/framework/base/ActionFilter.php b/framework/base/ActionFilter.php new file mode 100644 index 0000000..2655c5a --- /dev/null +++ b/framework/base/ActionFilter.php @@ -0,0 +1,45 @@ + + * @since 2.0 + */ +class Filter extends Behavior +{ + /** + * 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', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function beforeAction($event) + { + return $event->isValid; + } + + /** + * @param ActionEvent $event + * @return boolean + */ + public function afterAction($event) + { + + } +} \ No newline at end of file diff --git a/framework/base/Application.php b/framework/base/Application.php index 3dcbb26..31087e2 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -92,7 +92,6 @@ class Application extends Module private $_runtimePath; private $_ended = false; - private $_language; /** * @var string Used to reserve memory for fatal error handler. This memory diff --git a/framework/base/View.php b/framework/base/View.php index 36c90ad..baa1d10 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -322,46 +322,42 @@ class View extends Component $this->endWidget(); } - // - // /** - // * Begins fragment caching. - // * This method will display cached content if it is available. - // * If not, it will start caching and would expect an [[endCache()]] - // * call to end the cache and save the content into cache. - // * A typical usage of fragment caching is as follows, - // * - // * ~~~ - // * if($this->beginCache($id)) { - // * // ...generate content here - // * $this->endCache(); - // * } - // * ~~~ - // * - // * @param string $id a unique ID identifying the fragment to be cached. - // * @param array $properties initial property values for [[yii\widgets\OutputCache]] - // * @return boolean whether we need to generate content for caching. False if cached version is available. - // * @see endCache - // */ - // public function beginCache($id, $properties = array()) - // { - // $properties['id'] = $id; - // $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); - // if ($cache->getIsContentCached()) { - // $this->endCache(); - // return false; - // } else { - // return true; - // } - // } - // - // /** - // * Ends fragment caching. - // * This is an alias to [[endWidget()]] - // * @see beginCache - // */ - // public function endCache() - // { - // $this->endWidget(); - // } - // + /** + * Begins fragment caching. + * This method will display cached content if it is available. + * If not, it will start caching and would expect an [[endCache()]] + * call to end the cache and save the content into cache. + * A typical usage of fragment caching is as follows, + * + * ~~~ + * if($this->beginCache($id)) { + * // ...generate content here + * $this->endCache(); + * } + * ~~~ + * + * @param string $id a unique ID identifying the fragment to be cached. + * @param array $properties initial property values for [[\yii\widgets\OutputCache]] + * @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; + $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); + if ($cache->getIsContentCached()) { + $this->endCache(); + return false; + } else { + return true; + } + } + + /** + * Ends fragment caching. + */ + public function endCache() + { + $this->endWidget(); + } } \ No newline at end of file diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index ab87dfc..0409da3 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -4,91 +4,93 @@ namespace yii\i18n; use Yii; use yii\base\Component; +use yii\base\InvalidConfigException; class I18N extends Component { + /** + * @var array list of [[MessageSource]] configurations or objects. The array keys are message + * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations + * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end + * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + */ + public $translations; + + public function init() + { + if (!isset($this->translations['yii'])) { + $this->translations['yii'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@yii/messages', + ); + } + if (!isset($this->translations['app'])) { + $this->translations['app'] = array( + 'class' => 'yii\i18n\PhpMessageSource', + 'sourceLanguage' => 'en_US', + 'basePath' => '@app/messages', + ); + } + } + public function translate($message, $params = array(), $language = null) { if ($language === null) { $language = Yii::$app->language; } - if (strpos($message, '|') !== false && preg_match('/^([\w\-\.]+)\|(.*)/', $message, $matches)) { + // allow chars for category: word chars, ".", "-", "/","\" + if (strpos($message, '|') !== false && preg_match('/^([\w\-\\/\.\\\\]+)\|(.*)/', $message, $matches)) { $category = $matches[1]; $message = $matches[2]; } else { $category = 'app'; } -// $message = $this->getMessageSource($category)->translate($category, $message, $language); -// -// if (!is_array($params)) { -// $params = array($params); -// } -// -// if (isset($params[0])) { -// $message = $this->getPluralFormat($message, $params[0], $language); -// if (!isset($params['{n}'])) { -// $params['{n}'] = $params[0]; -// } -// unset($params[0]); -// } + $message = $this->getMessageSource($category)->translate($category, $message, $language); - return $params === array() ? $message : strtr($message, $params); - } + if (!is_array($params)) { + $params = array($params); + } - public function getLocale($language) - { + if (isset($params[0])) { + $message = $this->getPluralForm($message, $params[0], $language); + if (!isset($params['{n}'])) { + $params['{n}'] = $params[0]; + } + unset($params[0]); + } + return $params === array() ? $message : strtr($message, $params); } public function getMessageSource($category) { - return $category === 'yii' ? $this->getMessages() : $this->getCoreMessages(); - } - - private $_coreMessages; - private $_messages; - - public function getCoreMessages() - { - if (is_object($this->_coreMessages)) { - return $this->_coreMessages; - } elseif ($this->_coreMessages === null) { - return $this->_coreMessages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@yii/messages', - )); + if (isset($this->translations[$category])) { + $source = $this->translations[$category]; } else { - return $this->_coreMessages = Yii::createObject($this->_coreMessages); + // try wildcard matching + foreach ($this->translations as $pattern => $config) { + if (substr($pattern, -1) === '*' && strpos($category, rtrim($pattern, '*')) === 0) { + $source = $config; + break; + } + } } - } - - public function setCoreMessages($config) - { - $this->_coreMessages = $config; - } - - public function getMessages() - { - if (is_object($this->_messages)) { - return $this->_messages; - } elseif ($this->_messages === null) { - return $this->_messages = new PhpMessageSource(array( - 'sourceLanguage' => 'en_US', - 'basePath' => '@app/messages', - )); + if (isset($source)) { + return $source instanceof MessageSource ? $source : Yii::createObject($source); } else { - return $this->_messages = Yii::createObject($this->_messages); + throw new InvalidConfigException("Unable to locate message source for category '$category'."); } } - public function setMessages($config) + public function getLocale($language) { - $this->_messages = $config; + } - protected function getPluralFormat($message, $number, $language) + protected function getPluralForm($message, $number, $language) { if (strpos($message, '|') === false) { return $message; @@ -96,7 +98,7 @@ class I18N extends Component $chunks = explode('|', $message); $rules = $this->getLocale($language)->getPluralRules(); foreach ($rules as $i => $rule) { - if (isset($chunks[$i]) && self::evaluate($rule, $number)) { + if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; } } @@ -110,7 +112,7 @@ class I18N extends Component * @param mixed $n the number value * @return boolean the expression result */ - protected static function evaluate($expression, $n) + protected function evaluate($expression, $n) { return @eval("return $expression;"); } diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 5c7374a..6b12353 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -36,6 +36,18 @@ class PhpMessageSource extends MessageSource * the "messages" subdirectory of the application directory (e.g. "protected/messages"). */ public $basePath = '@app/messages'; + /** + * @var array mapping between message categories and the corresponding message file paths. + * The file paths are relative to [[basePath]]. For example, + * + * ~~~ + * array( + * 'core' => 'core.php', + * 'ext' => 'extensions.php', + * ) + * ~~~ + */ + public $fileMap; /** * Loads the message translation for the specified language and category. @@ -45,7 +57,14 @@ class PhpMessageSource extends MessageSource */ protected function loadMessages($category, $language) { - $messageFile = Yii::getAlias($this->basePath) . "/$language/$category.php"; + $messageFile = Yii::getAlias($this->basePath) . "/$language/"; + if (isset($this->fileMap[$category])) { + $messageFile .= $this->fileMap[$category]; + } elseif (($pos = strrpos($category, '\\')) !== false) { + $messageFile .= (substr($category, $pos) . '.php'); + } else { + $messageFile .= "$category.php"; + } if (is_file($messageFile)) { $messages = include($messageFile); if (!is_array($messages)) { @@ -53,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("Message file not found: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); return array(); } } diff --git a/framework/logging/Target.php b/framework/logging/Target.php index 32d12d8..b88e78d 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -192,8 +192,7 @@ abstract class Target extends \yii\base\Component $matched = empty($this->categories); foreach ($this->categories as $category) { - $prefix = rtrim($category, '*'); - if (strpos($message[2], $prefix) === 0 && ($message[2] === $category || $prefix !== $category)) { + if ($message[2] === $category || substr($category, -1) === '*' && strpos($message[2], rtrim($category, '*')) === 0) { $matched = true; break; } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php new file mode 100644 index 0000000..e6805f5 --- /dev/null +++ b/framework/widgets/FragmentCache.php @@ -0,0 +1,294 @@ + + * @since 2.0 + */ +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. + */ + 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, + *
+ * array( + * 'class'=>'CDbCacheDependency', + * '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. + */ + public $varyByExpression; + /** + * @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. + */ + public $requestTypes; + + private $_key; + private $_cache; + private $_contentCached; + private $_content; + private $_actions; + + /** + * Marks the start of content to be cached. + * Content displayed after this method call and before {@link endCache()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + public function init() + { + if ($this->getIsContentCached()) { + $this->replayActions(); + } elseif ($this->_cache !== null) { + $this->getController()->getCachingStack()->push($this); + ob_start(); + ob_implicit_flush(false); + } + } + + /** + * Marks the end of content to be cached. + * Content displayed before this method call and after {@link init()} + * will be captured and saved in cache. + * This method does nothing if valid content is already found in cache. + */ + 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 (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; + } + } + } + + /** + * @return boolean whether the content can be found from cache + */ + public function getIsContentCached() + { + if ($this->_contentCached !== null) { + return $this->_contentCached; + } else { + return $this->_contentCached = $this->checkContentCache(); + } + } + + /** + * Looks for content in cache. + * @return boolean whether the content is found in cache. + */ + protected function checkContentCache() + { + 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; + } + } + return false; + } + + /** + * @return ICache the cache used for caching the content. + */ + protected function getCache() + { + return Yii::app()->getComponent($this->cacheID); + } + + /** + * 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 + */ + protected function getBaseCacheKey() + { + return self::CACHE_KEY_PREFIX . $this->getId() . '.'; + } + + /** + * 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 + */ + protected function getCacheKey() + { + 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; + } + $key .= '.'; + + return $this->_key = $key; + } + } + + /** + * 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. + */ + protected function replayActions() + { + 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]))); + } + } + } +} \ No newline at end of file diff --git a/todo.md b/todo.md index 4d5343a..f102f41 100644 --- a/todo.md +++ b/todo.md @@ -39,7 +39,6 @@ memo * consider to be released as a separate tool for user app docs - i18n * consider using PHP built-in support and data - * message translations, choice format * formatting: number and date * parsing?? * make dates/date patterns uniform application-wide including JUI, formats etc.