From 41ea94853bb73f67f1bbf16a855c3fb3f14fbc1a Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 19 Apr 2013 15:19:29 -0400 Subject: [PATCH] refactoring and documentation for asset/script management. --- framework/base/View.php | 366 +++++++++++++++++++++++++++++++++++++++-- framework/base/ViewContent.php | 235 -------------------------- framework/web/Application.php | 6 +- framework/web/AssetBundle.php | 38 +++-- framework/web/AssetManager.php | 10 +- 5 files changed, 388 insertions(+), 267 deletions(-) delete mode 100644 framework/base/ViewContent.php diff --git a/framework/base/View.php b/framework/base/View.php index a794e08..b791743 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -10,6 +10,7 @@ namespace yii\base; use Yii; use yii\base\Application; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * View represents a view object in the MVC pattern. @@ -22,22 +23,48 @@ use yii\helpers\FileHelper; class View extends Component { /** - * @event Event an event that is triggered by [[renderFile()]] right before it renders a view file. + * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; /** - * @event Event an event that is triggered by [[renderFile()]] right after it renders a view file. + * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file. */ const EVENT_AFTER_RENDER = 'afterRender'; /** - * @var object the object that owns this view. This can be a controller, a widget, or any other object. + * The location of registered JavaScript code block or files. + * This means the location is in the head section. */ - public $context; + const POS_HEAD = 1; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the beginning of the body section. + */ + const POS_BEGIN = 2; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the end of the body section. + */ + const POS_END = 3; + /** + * This is internally used as the placeholder for receiving the content registered for the head section. + */ + const PL_HEAD = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. + */ + const PL_BODY_BEGIN = ''; /** - * @var ViewContent + * This is internally used as the placeholder for receiving the content registered for the end of the body section. */ - public $page; + const PL_BODY_END = ''; + + + /** + * @var object the context under which the [[renderFile()]] method is being invoked. + * This can be a controller, a widget, or any other object. + */ + public $context; /** * @var mixed custom parameters that are shared among view templates. */ @@ -48,32 +75,75 @@ class View extends Component */ public $renderer; /** - * @var Theme|array the theme object or the configuration array for creating the theme. + * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. */ public $theme; /** * @var array a list of named output blocks. The keys are the block names and the values * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] - * to capture small fragments of a view. They can be later accessed at somewhere else + * to capture small fragments of a view. They can be later accessed somewhere else * through this property. */ public $blocks; /** * @var Widget[] the widgets that are currently being rendered (not ended). This property - * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it. + * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + * @internal */ public $widgetStack = array(); /** * @var array a list of currently active fragment cache widgets. This property - * is used internally to implement the content caching feature. Do not modify it. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $cacheStack = array(); /** * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $dynamicPlaceholders = array(); + /** + * @var array the registered asset bundles. The keys are the bundle names, and the values + * are the corresponding [[AssetBundle]] objects. + * @see registerAssetBundle + */ + public $assetBundles; + /** + * @var string the page title + */ + public $title; + /** + * @var array the registered meta tags. + * @see registerMetaTag + */ + public $metaTags; + /** + * @var array the registered link tags. + * @see registerLinkTag + */ + public $linkTags; + /** + * @var array the registered CSS code blocks. + * @see registerCss + */ + public $css; + /** + * @var array the registered CSS files. + * @see registerCssFile + */ + public $cssFiles; + /** + * @var array the registered JS code blocks + * @see registerJs + */ + public $js; + /** + * @var array the registered JS files. + * @see registerJsFile + */ + public $jsFiles; /** @@ -88,11 +158,6 @@ class View extends Component if (is_array($this->theme)) { $this->theme = Yii::createObject($this->theme); } - if (is_array($this->page)) { - $this->page = Yii::createObject($this->page); - } else { - $this->page = new ViewContent; - } } /** @@ -445,4 +510,273 @@ class View extends Component { $this->endWidget(); } + + + private $_assetManager; + + /** + * Registers the asset manager being used by this view object. + * @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component. + */ + public function getAssetManager() + { + return $this->_assetManager ?: Yii::$app->getAssetManager(); + } + + /** + * Sets the asset manager. + * @param \yii\web\AssetManager $value the asset manager + */ + public function setAssetManager($value) + { + $this->_assetManager = $value; + } + + /** + * Marks the beginning of an HTML page. + */ + public function beginPage() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Marks the ending of an HTML page. + */ + public function endPage() + { + $content = ob_get_clean(); + echo strtr($content, array( + self::PL_HEAD => $this->renderHeadHtml(), + self::PL_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PL_BODY_END => $this->renderBodyEndHtml(), + )); + + unset( + $this->assetBundles, + $this->metaTags, + $this->linkTags, + $this->css, + $this->cssFiles, + $this->js, + $this->jsFiles + ); + } + + /** + * Marks the beginning of an HTML body section. + */ + public function beginBody() + { + echo self::PL_BODY_BEGIN; + } + + /** + * Marks the ending of an HTML body section. + */ + public function endBody() + { + echo self::PL_BODY_END; + } + + /** + * Marks the position of an HTML head section. + */ + public function head() + { + echo self::PL_HEAD; + } + + /** + * Registers the named asset bundle. + * All dependent asset bundles will be registered. + * @param string $name the name of the asset bundle. + * @throws InvalidConfigException if the asset bundle does not exist or a cyclic dependency is detected + */ + public function registerAssetBundle($name) + { + if (!isset($this->assetBundles[$name])) { + $am = $this->getAssetManager(); + $bundle = $am->getBundle($name); + if ($bundle !== null) { + $this->assetBundles[$name] = false; + $bundle->registerAssets($this); + $this->assetBundles[$name] = true; + } else { + throw new InvalidConfigException("Unknown asset bundle: $name"); + } + } elseif ($this->assetBundles[$name] === false) { + throw new InvalidConfigException("A cyclic dependency is detected for bundle '$name'."); + } + } + + /** + * Registers a meta tag. + * @param array $options the HTML attributes for the meta tag. + * @param string $key the key that identifies the meta tag. If two meta tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new meta tag + * will be appended to the existing ones. + */ + public function registerMetaTag($options, $key = null) + { + if ($key === null) { + $this->metaTags[] = Html::tag('meta', '', $options); + } else { + $this->metaTags[$key] = Html::tag('meta', '', $options); + } + } + + /** + * Registers a link tag. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the link tag. If two link tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new link tag + * will be appended to the existing ones. + */ + public function registerLinkTag($options, $key = null) + { + if ($key === null) { + $this->linkTags[] = Html::tag('link', '', $options); + } else { + $this->linkTags[$key] = Html::tag('link', '', $options); + } + } + + /** + * Registers a CSS code block. + * @param string $css the CSS code block to be registered + * @param array $options the HTML attributes for the style tag. + * @param string $key the key that identifies the CSS code block. If null, it will use + * $css as the key. If two CSS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCss($css, $options = array(), $key = null) + { + $key = $key ?: $css; + $this->css[$key] = Html::style($css, $options); + } + + /** + * Registers a CSS file. + * @param string $url the CSS file to be registered. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the CSS script file. If null, it will use + * $url as the key. If two CSS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCssFile($url, $options = array(), $key = null) + { + $key = $key ?: $url; + $this->cssFiles[$key] = Html::cssFile($url, $options); + } + + /** + * Registers a JS code block. + * @param string $js the JS code block to be registered + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS code block. If null, it will use + * $js as the key. If two JS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJs($js, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $js; + $this->js[$position][$key] = Html::script($js, $options); + } + + /** + * Registers a JS file. + * @param string $url the JS file to be registered. + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS script file. If null, it will use + * $url as the key. If two JS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJsFile($url, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $url; + $this->jsFiles[$position][$key] = Html::jsFile($url, $options); + } + + /** + * Renders the content to be inserted in the head section. + * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files. + * @return string the rendered content + */ + protected function renderHeadHtml() + { + $lines = array(); + if (!empty($this->metaTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->linkTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->cssFiles)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->css)) { + $lines[] = implode("\n", $this->css); + } + if (!empty($this->jsFiles[self::POS_HEAD])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); + } + if (!empty($this->js[self::POS_HEAD])) { + $lines[] = implode("\n", $this->js[self::POS_HEAD]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the beginning of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyBeginHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); + } + if (!empty($this->js[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->js[self::POS_BEGIN]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the end of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyEndHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_END])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_END]); + } + if (!empty($this->js[self::POS_END])) { + $lines[] = implode("\n", $this->js[self::POS_END]); + } + return implode("\n", $lines); + } } \ No newline at end of file diff --git a/framework/base/ViewContent.php b/framework/base/ViewContent.php deleted file mode 100644 index c7a3bc4..0000000 --- a/framework/base/ViewContent.php +++ /dev/null @@ -1,235 +0,0 @@ - - * @since 2.0 - */ -class ViewContent extends Component -{ - const POS_HEAD = 1; - const POS_BEGIN = 2; - const POS_END = 3; - - const TOKEN_HEAD = ''; - const TOKEN_BODY_BEGIN = ''; - const TOKEN_BODY_END = ''; - - public $assetBundles; - public $title; - public $metaTags; - public $linkTags; - public $css; - public $cssFiles; - public $js; - public $jsFiles; - public $jsInHead; - public $jsFilesInHead; - public $jsInBody; - public $jsFilesInBody; - - public function reset() - { - $this->assetBundles = null; - $this->title = null; - $this->metaTags = null; - $this->linkTags = null; - $this->css = null; - $this->cssFiles = null; - $this->js = null; - $this->jsFiles = null; - $this->jsInHead = null; - $this->jsFilesInHead = null; - $this->jsInBody = null; - $this->jsFilesInBody = null; - } - - private $_assetManager; - - /** - * @return \yii\web\AssetManager - */ - public function getAssetManager() - { - return $this->_assetManager ?: Yii::$app->getAssets(); - } - - public function setAssetManager($value) - { - $this->_assetManager = $value; - } - - public function begin() - { - ob_start(); - ob_implicit_flush(false); - } - - public function end() - { - $content = ob_get_clean(); - echo $this->populate($content); - } - - public function beginBody() - { - echo self::TOKEN_BODY_BEGIN; - } - - public function endBody() - { - echo self::TOKEN_BODY_END; - } - - public function head() - { - echo self::TOKEN_HEAD; - } - - public function registerAssetBundle($name) - { - if (!isset($this->assetBundles[$name])) { - $am = $this->getAssetManager(); - $bundle = $am->getBundle($name); - if ($bundle !== null) { - $this->assetBundles[$name] = false; - $bundle->registerAssets($this, $am); - $this->assetBundles[$name] = true; - } else { - throw new InvalidConfigException("Unknown asset bundle: $name"); - } - } elseif ($this->assetBundles[$name] === false) { - throw new InvalidConfigException("A cyclic dependency is detected for bundle '$name'."); - } - } - - public function registerMetaTag($options, $key = null) - { - if ($key === null) { - $this->metaTags[] = Html::tag('meta', '', $options); - } else { - $this->metaTags[$key] = Html::tag('meta', '', $options); - } - } - - public function registerLinkTag($options, $key = null) - { - if ($key === null) { - $this->linkTags[] = Html::tag('link', '', $options); - } else { - $this->linkTags[$key] = Html::tag('link', '', $options); - } - } - - public function registerCss($css, $options = array(), $key = null) - { - $key = $key ?: $css; - $this->css[$key] = Html::style($css, $options); - } - - public function registerCssFile($url, $options = array(), $key = null) - { - $key = $key ?: $url; - $this->cssFiles[$key] = Html::cssFile($url, $options); - } - - public function registerJs($js, $options = array(), $key = null) - { - $position = isset($options['position']) ? $options['position'] : self::POS_END; - unset($options['position']); - $key = $key ?: $js; - $html = Html::script($js, $options); - if ($position == self::POS_END) { - $this->js[$key] = $html; - } elseif ($position == self::POS_HEAD) { - $this->jsInHead[$key] = $html; - } elseif ($position == self::POS_BEGIN) { - $this->jsInBody[$key] = $html; - } else { - throw new InvalidParamException("Unknown position: $position"); - } - } - - public function registerJsFile($url, $options = array(), $key = null) - { - $position = isset($options['position']) ? $options['position'] : self::POS_END; - unset($options['position']); - $key = $key ?: $url; - $html = Html::jsFile($url, $options); - if ($position == self::POS_END) { - $this->jsFiles[$key] = $html; - } elseif ($position == self::POS_HEAD) { - $this->jsFilesInHead[$key] = $html; - } elseif ($position == self::POS_BEGIN) { - $this->jsFilesInBody[$key] = $html; - } else { - throw new InvalidParamException("Unknown position: $position"); - } - } - - protected function populate($content) - { - return strtr($content, array( - self::TOKEN_HEAD => $this->getHeadHtml(), - self::TOKEN_BODY_BEGIN => $this->getBodyBeginHtml(), - self::TOKEN_BODY_END => $this->getBodyEndHtml(), - )); - } - - protected function getHeadHtml() - { - $lines = array(); - if (!empty($this->metaTags)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->linkTags)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->cssFiles)) { - $lines[] = implode("\n", $this->cssFiles); - } - if (!empty($this->css)) { - $lines[] = implode("\n", $this->css); - } - if (!empty($this->jsFilesInHead)) { - $lines[] = implode("\n", $this->jsFilesInHead); - } - if (!empty($this->jsInHead)) { - $lines[] = implode("\n", $this->jsInHead); - } - return implode("\n", $lines); - } - - protected function getBodyBeginHtml() - { - $lines = array(); - if (!empty($this->jsFilesInBody)) { - $lines[] = implode("\n", $this->jsFilesInBody); - } - if (!empty($this->jsInHead)) { - $lines[] = implode("\n", $this->jsInBody); - } - return implode("\n", $lines); - } - - protected function getBodyEndHtml() - { - $lines = array(); - if (!empty($this->jsFiles)) { - $lines[] = implode("\n", $this->jsFiles); - } - if (!empty($this->js)) { - $lines[] = implode("\n", $this->js); - } - return implode("\n", $lines); - } -} \ No newline at end of file diff --git a/framework/web/Application.php b/framework/web/Application.php index 90f7292..3387044 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -101,9 +101,9 @@ class Application extends \yii\base\Application * Returns the asset manager. * @return AssetManager the asset manager component */ - public function getAssets() + public function getAssetManager() { - return $this->getComponent('assets'); + return $this->getComponent('assetManager'); } /** @@ -126,7 +126,7 @@ class Application extends \yii\base\Application 'user' => array( 'class' => 'yii\web\User', ), - 'assets' => array( + 'assetManager' => array( 'class' => 'yii\web\AssetManager', ), )); diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php index f82173b..4108b07 100644 --- a/framework/web/AssetBundle.php +++ b/framework/web/AssetBundle.php @@ -12,6 +12,14 @@ use yii\base\InvalidConfigException; use yii\base\Object; /** + * AssetBundle represents a collection of asset files, such as CSS, JS, images. + * + * Each asset bundle has a unique name that globally identifies it among all asset bundles + * used in an application. + * + * An asset bundle can depend on other asset bundles. When registering an asset bundle + * with a view, all its dependent asset bundles will be automatically registered. + * * @author Qiang Xue * @since 2.0 */ @@ -66,7 +74,7 @@ class AssetBundle extends Object * * Each JavaScript file may be associated with options. In this case, the array key * should be the JavaScript file path, while the corresponding array value should - * be the option array. The options will be passed to [[ViewContent::registerJsFile()]]. + * be the option array. The options will be passed to [[View::registerJsFile()]]. */ public $js = array(); /** @@ -78,7 +86,7 @@ class AssetBundle extends Object * * Each CSS file may be associated with options. In this case, the array key * should be the CSS file path, while the corresponding array value should - * be the option array. The options will be passed to [[ViewContent::registerCssFile()]]. + * be the option array. The options will be passed to [[View::registerCssFile()]]. */ public $css = array(); /** @@ -108,14 +116,20 @@ class AssetBundle extends Object } /** - * @param \yii\base\ViewContent $page - * @param AssetManager $am - * @throws InvalidConfigException + * Registers the CSS and JS files with the given view. + * This method will first register all dependent asset bundles. + * It will then try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding + * CSS or JS files using [[AssetManager::converter|asset converter]]. + * @param \yii\base\View $view the view that the asset files to be registered with. + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. */ - public function registerAssets($page, $am) + public function registerAssets($view) { + $am = $view->getAssetManager(); + foreach ($this->depends as $name) { - $page->registerAssetBundle($name); + $view->registerAssetBundle($name); } if ($this->sourcePath !== null) { @@ -128,23 +142,23 @@ class AssetBundle extends Object $js = is_string($options) ? $options : $js; if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $js = $converter->convert(ltrim($js, '/'), $this->basePath, $this->baseUrl); + $js = $converter->convert($js, $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } } - $page->registerJsFile($js, is_array($options) ? $options : array()); + $view->registerJsFile($js, is_array($options) ? $options : array()); } foreach ($this->css as $css => $options) { $css = is_string($options) ? $options : $css; - if (strpos($css, '//') !== 0 && strpos($css, '://') === false) { + if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { if (isset($this->basePath, $this->baseUrl)) { - $css = $converter->convert(ltrim($css, '/'), $this->basePath, $this->baseUrl); + $css = $converter->convert($css, $this->basePath, $this->baseUrl); } else { throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); } } - $page->registerCssFile($css, is_array($options) ? $options : array()); + $view->registerCssFile($css, is_array($options) ? $options : array()); } } } \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index c4ad385..95dcbd2 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -14,6 +14,7 @@ use yii\base\InvalidParamException; use yii\helpers\FileHelper; /** + * AssetManager manages asset bundles and asset publishing. * * @author Qiang Xue * @since 2.0 @@ -135,7 +136,8 @@ class AssetManager extends Component private $_converter; /** - * @return IAssetConverter + * Returns the asset converter. + * @return IAssetConverter the asset converter. */ public function getConverter() { @@ -149,6 +151,12 @@ class AssetManager extends Component return $this->_converter; } + /** + * Sets the asset converter. + * @param array|IAssetConverter $value the asset converter. This can be either + * an object implementing the [[IAssetConverter]] interface, or a configuration + * array that can be used to create the asset converter object. + */ public function setConverter($value) { $this->_converter = $value;