From 739f3b13e435b43eaf73e4d462d66e3ed6a23c46 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 10 Aug 2014 22:05:48 +0400 Subject: [PATCH] Fixes #4553: Smarty enhancements --- docs/guide/tutorial-template-engines.md | 174 ++++++++- extensions/smarty/CHANGELOG.md | 22 +- extensions/smarty/Extension.php | 429 +++++++++++++++++++++ extensions/smarty/ViewRenderer.php | 220 ++++++++++- tests/unit/extensions/smarty/ViewRendererTest.php | 45 +++ tests/unit/extensions/smarty/views/changeTitle.tpl | 3 + tests/unit/extensions/smarty/views/extends1.tpl | 5 + tests/unit/extensions/smarty/views/extends2.tpl | 5 + tests/unit/extensions/smarty/views/extends3.tpl | 5 + tests/unit/extensions/smarty/views/form.tpl | 9 + tests/unit/extensions/smarty/views/layout.tpl | 16 + 11 files changed, 900 insertions(+), 33 deletions(-) create mode 100644 extensions/smarty/Extension.php create mode 100644 tests/unit/extensions/smarty/views/changeTitle.tpl create mode 100644 tests/unit/extensions/smarty/views/extends1.tpl create mode 100644 tests/unit/extensions/smarty/views/extends2.tpl create mode 100644 tests/unit/extensions/smarty/views/extends3.tpl create mode 100644 tests/unit/extensions/smarty/views/form.tpl create mode 100644 tests/unit/extensions/smarty/views/layout.tpl diff --git a/docs/guide/tutorial-template-engines.md b/docs/guide/tutorial-template-engines.md index 3a7ad8b..1e22417 100644 --- a/docs/guide/tutorial-template-engines.md +++ b/docs/guide/tutorial-template-engines.md @@ -106,9 +106,9 @@ Aliased class import: {{ use({'alias' => '/app/widgets/MyWidget'}) }} ``` -#### Referencing other views +#### Referencing other templates -There are two ways of referencing views in `include` and `extends` statements: +There are two ways of referencing templates in `include` and `extends` statements: ``` {% include "comment.twig" %} @@ -118,8 +118,8 @@ There are two ways of referencing views in `include` and `extends` statements: {% extends "@app/views/layouts/2columns.twig" %} ``` -In the first case the view will be searched relatively to the path current view is in. For `comment.twig` and `post.twig` -that means these will be searched in the same directory as the view that's rendered currently. +In the first case the view will be searched relatively to the current template path. For `comment.twig` and `post.twig` +that means these will be searched in the same directory as the currently rendered template. In the second case we're using path aliases. All the Yii aliases such as `@app` are available by default. @@ -264,8 +264,9 @@ Then in the template you can apply filter using the following syntax: Smarty ------ -To use Smarty, you need to create templates in files that have the `.tpl` extension (or use another file extension but configure the component accordingly). Unlike standard view files, when using Smarty you must include the extension in your `$this->render()` -or `$this->renderPartial()` controller calls: +To use Smarty, you need to create templates in files that have the `.tpl` extension (or use another file extension but +configure the component accordingly). Unlike standard view files, when using Smarty you must include the extension in +your `$this->render()` or `$this->renderPartial()` controller calls: ```php return $this->render('renderer.tpl', ['username' => 'Alex']); @@ -277,20 +278,173 @@ The best resource to learn Smarty template syntax is its official documentation [www.smarty.net](http://www.smarty.net/docs/en/). Additionally there are Yii-specific syntax extensions described below. -#### Additional functions +#### Setting object properties + +There's a special function called `set` that allows you to set common properties of the view and controller. Currently +available properties are `title`, `theme` and `layout`: + +``` +{set title="My Page"} +{set theme="frontend"} +{set layout="main.tpl"} +``` + +For title there's dedicated block as well: + +``` +{title}My Page{/title} +``` + +#### Setting meta tags + +Meta tags could be set like to following: + +``` +{meta keywords="Yii,PHP,Smarty,framework"} +``` + +There's also dedicated block for description: + +``` +{description}This is my page about Smarty extension{/description} +``` + +#### Calling object methods + +Sometimes you need calling + +#### Importing static classes, using widgets as functions and blocks + +You can import additional static classes right in the template: + +``` +{use class="yii\helpers\Html"} +{Html::mailto('eugenia@example.com')} +``` + +If you want you can set custom alias: + +``` +{use class="yii\helpers\Html" as="Markup"} +{Markup::mailto('eugenia@example.com')} +``` + +Extension helps using widgets in convenient way converting their syntax to function calls or blocks. For regular widgets +function could be used like the following: + +``` +{use class='@yii\grid\GridView' type='function'} +{GridView dataProvider=$provider} +``` + +For widgets with `begin` and `end` methods such as ActiveForm it's better to use block: + +``` +{use class='yii\widgets\ActiveForm' type='block'} +{ActiveForm assign='form' id='login-form' action='/form-handler' options=['class' => 'form-horizontal']} + {$form->field($model, 'firstName')} +
+
+ +
+
+{/ActiveForm} +``` + +If you're using particular widget a lot, it is a good idea to declare it in application config and remove `{use class` +call from templates: -Yii adds the following construct to the standard Smarty syntax: +```php +'components' => [ + 'view' => [ + // ... + 'renderers' => [ + 'tpl' => [ + 'class' => 'yii\smarty\ViewRenderer', + 'widgets' => [ + 'blocks' => [ + 'ActiveForm' => '\yii\widgets\ActiveForm', + ], + ], + ], + ], + ], +], +``` + +#### Referencing other templates + +There are two main ways of referencing templates in `include` and `extends` statements: + +``` +{include 'comment.tpl'} +{extends 'post.tpl'} + +{include '@app/views/snippets/avatar.tpl'} +{extends '@app/views/layouts/2columns.tpl'} +``` + +In the first case the view will be searched relatively to the current template path. For `comment.tpl` and `post.tpl` +that means these will be searched in the same directory as the currently rendered template. + +In the second case we're using path aliases. All the Yii aliases such as `@app` are available by default. + +#### CSS, JavaScript and asset bundles + +In order to register JavaScript and CSS files the following syntax could be used: + +``` +{registerJsFile url='http://maps.google.com/maps/api/js?sensor=false' position='POS_END'} +{registerCssFile url='@assets/css/normalizer.css'} +``` + +If you need JavaScript and CSS directly in the template there are convenient blocks: + +``` +{registerJs key='show' position='POS_LOAD'} + $("span.show").replaceWith('
'); +{/registerJs} + +{registerCss} +div.header { + background-color: #3366bd; + color: white; +} +{/registerCss} +``` + +Asset bundles could be registered the following way: + +``` +{use class="yii\web\JqueryAsset"} +{JqueryAsset::register($this)|void} +``` + +Here we're using `void` modifier because we don't need method call result. + +#### URLs + +There are two functions you can use for building URLs: ```php {$post.title} +{$post.title} ``` -Internally, the `path()` function calls Yii's `Url::to()` method. +`path` generates relative URL while `url` generates absolute one. Internally both are using [[\yii\helpers\Url]]. #### Additional variables -Within Smarty templates, you can also make use of these variables: +Within Smarty templates the following variables are always defined: - `$app`, which equates to `\Yii::$app` - `$this`, which equates to the current `View` object +#### Accessing config params + +Yii parameters that are available in your application through `Yii::$app->params->something` could be used the following +way: + +``` +`{#something#}` +``` diff --git a/extensions/smarty/CHANGELOG.md b/extensions/smarty/CHANGELOG.md index e5f797d..6a92c4e 100644 --- a/extensions/smarty/CHANGELOG.md +++ b/extensions/smarty/CHANGELOG.md @@ -4,8 +4,26 @@ Yii Framework 2 smarty extension Change Log 2.0.0-rc under development -------------------------- -- no changes in this release. - +- Enh #4619 (samdark, hwmaier) + - New functions: + - `url` generates absolute URL. + - `set` allows setting commonly used view paramters: `title`, `theme` and `layout`. + - `meta` registers meta tag. + - `registerJsFile` registers JavaScript file. + - `registerCssFile` registers CSS file. + - `use` allows importing classes to the template and optionally provides these as functions and blocks. + - New blocks: + - `title`. + - `description`. + - `registerJs`. + - `registerCss`. + - New modifier `void` that allows calling functions and ignoring result. + - Moved most of Yii custom syntax into `\yii\smarty\Extension` class that could be extended via `extensionClass` property. + - Added ability to set Smarty options via config using `options`. + - Added `imports` property that accepts an array of classes imported into template namespace. + - Added `widgets` property that can be used to import widgets as Smarty tags. + - `Yii::$app->params['paramKey']` values are now accessible as Smarty config variables `{#paramKey#}`. + - Added ability to use Yii aliases in `extends` and `require`. 2.0.0-beta April 13, 2014 ------------------------- diff --git a/extensions/smarty/Extension.php b/extensions/smarty/Extension.php new file mode 100644 index 0000000..4359dc7 --- /dev/null +++ b/extensions/smarty/Extension.php @@ -0,0 +1,429 @@ + + * @author Henrik Maier + */ +class Extension +{ + /** + * @var ViewRenderer + */ + protected $viewRenderer; + + /** + * @var Smarty + */ + protected $smarty; + + /** + * @param ViewRenderer $viewRenderer + * @param Smarty $smarty + */ + public function __construct($viewRenderer, $smarty) + { + $this->viewRenderer = $viewRenderer; + $smarty = $this->smarty = $smarty; + + $smarty->registerPlugin('function', 'path', [$this, 'functionPath']); + $smarty->registerPlugin('function', 'url', [$this, 'functionUrl']); + $smarty->registerPlugin('function', 'set', [$this, 'functionSet']); + $smarty->registerPlugin('function', 'meta', [$this, 'functionMeta']); + $smarty->registerPlugin('function', 'registerJsFile', [$this, 'functionRegisterJsFile']); + $smarty->registerPlugin('function', 'registerCssFile', [$this, 'functionRegisterCssFile']); + $smarty->registerPlugin('block', 'title', [$this, 'blockTitle']); + $smarty->registerPlugin('block', 'description', [$this, 'blockDescription']); + $smarty->registerPlugin('block', 'registerJs', [$this, 'blockJavaScript']); + $smarty->registerPlugin('block', 'registerCss', [$this, 'blockCss']); + $smarty->registerPlugin('compiler', 'use', [$this, 'compilerUse']); + $smarty->registerPlugin('modifier', 'void', [$this, 'modifierVoid']); + } + + /** + * Smarty template function to get relative URL for using in links + * + * Usage is the following: + * + * {path route='blog/view' alias=$post.alias user=$user.id} + * + * where route is Yii route and the rest of parameters are passed as is. + * + * @param array $params + * @param \Smarty_Internal_Template $template + * + * @return string + */ + public function functionPath($params, \Smarty_Internal_Template $template) + { + if (!isset($params['route'])) { + trigger_error("path: missing 'route' parameter"); + } + + array_unshift($params, $params['route']) ; + unset($params['route']); + + return Url::to($params, true); + } + + /** + * Smarty template function to get absolute URL for using in links + * + * Usage is the following: + * + * {path route='blog/view' alias=$post.alias user=$user.id} + * + * where route is Yii route and the rest of parameters are passed as is. + * + * @param array $params + * @param \Smarty_Internal_Template $template + * + * @return string + */ + public function functionUrl($params, \Smarty_Internal_Template $template) + { + if (!isset($params['route'])) { + trigger_error("path: missing 'route' parameter"); + } + + array_unshift($params, $params['route']) ; + unset($params['route']); + + return Url::to($params, true); + } + + /** + * Smarty compiler function plugin + * Usage is the following: + * + * {use class="app\assets\AppAsset"} + * {use class="yii\helpers\Html"} + * {use class='yii\widgets\ActiveForm' type='block'} + * {use class='@app\widgets\MyWidget' as='my_widget' type='function'} + * + * Supported attributes: class, as, type. Type defaults to 'static'. + * + * @param $params + * @param \Smarty_Internal_Template $template + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function compilerUse($params, $template) + { + if (!isset($params['class'])) { + trigger_error("use: missing 'class' parameter"); + } + // Compiler plugin parameters may include quotes, so remove them + foreach ($params as $key => $value) { + $params[$key] = trim($value, '\'""'); + } + + $class = $params['class']; + $alias = ArrayHelper::getValue($params, 'as', basename($params['class'])); + $type = ArrayHelper::getValue($params, 'type', 'static'); + + // Register the class during compile time + $this->smarty->registerClass($alias, $class); + + if ($type === 'block') { + // Register widget tag during compile time + $this->viewRenderer->widgets['blocks'][$alias] = $class; + $this->smarty->registerPlugin('block', $alias, [$this->viewRenderer, '_widget_block__' . $alias]); + + // Inject code to re-register widget tag during run-time + return <<getGlobal('_viewRenderer')->widgets['blocks']['$alias'] = '$class'; + try { + \$_smarty_tpl->registerPlugin('block', '$alias', [\$_smarty_tpl->getGlobal('_viewRenderer'), '_widget_block__$alias']); + } + catch (SmartyException \$e) { + /* Ignore already registered exception during first execution after compilation */ + } +?> +PHP; + } elseif ($type === 'function') { + // Register widget tag during compile time + $this->viewRenderer->widgets['functions'][$alias] = $class; + $this->smarty->registerPlugin('function', $alias, [$this->viewRenderer, '_widget_function__' . $alias]); + + // Inject code to re-register widget tag during run-time + return <<getGlobal('_viewRenderer')->widgets['functions']['$alias'] = '$class'; + try { + \$_smarty_tpl->registerPlugin('function', '$alias', [\$_smarty_tpl->getGlobal('_viewRenderer'), '_widget_function__$alias']); + } + catch (SmartyException \$e) { + /* Ignore already registered exception during first execution after compilation */ + } +?> +PHP; + } + } + + /** + * Smarty modifier plugin + * Converts any output to void + * @param mixed $arg + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function modifierVoid($arg) + { + return; + } + + /** + * Smarty function plugin + * Usage is the following: + * + * {set title="My Page"} + * {set theme="frontend"} + * {set layout="main.tpl"} + * + * Supported attributes: title, theme, layout + * + * @param $params + * @param \Smarty_Internal_Template $template + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function functionSet($params, $template) + { + if (isset($params['title'])) { + $template->tpl_vars['this']->value->title = Yii::$app->getView()->title = ArrayHelper::remove($params, 'title'); + } + if (isset($params['theme'])) { + $template->tpl_vars['this']->value->theme = Yii::$app->getView()->theme = ArrayHelper::remove($params, 'theme'); + } + if (isset($params['layout'])) { + Yii::$app->controller->layout = ArrayHelper::remove($params, 'layout'); + } + + // We must have consumed all allowed parameters now, otherwise raise error + if (!empty($params)) { + trigger_error('set: Unsupported parameter attribute'); + } + } + + /** + * Smarty function plugin + * Usage is the following: + * + * {meta keywords="Yii,PHP,Smarty,framework"} + * + * Supported attributes: any; all attributes are passed as + * parameter array to Yii's registerMetaTag function. + * + * @param $params + * @param \Smarty_Internal_Template $template + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function functionMeta($params, $template) + { + $key = isset($params['name']) ? $params['name'] : null; + + Yii::$app->getView()->registerMetaTag($params, $key); + } + + /** + * Smarty block function plugin + * Usage is the following: + * + * {title} Web Site Login {/title} + * + * Supported attributes: none. + * + * @param $params + * @param $content + * @param \Smarty_Internal_Template $template + * @param $repeat + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function blockTitle($params, $content, $template, &$repeat) + { + if ($content !== null) { + Yii::$app->getView()->title = $content; + } + } + + /** + * Smarty block function plugin + * Usage is the following: + * + * {description} + * The text between the opening and closing tags is added as + * meta description tag to the page output. + * {/description} + * + * Supported attributes: none. + * + * @param $params + * @param $content + * @param \Smarty_Internal_Template $template + * @param $repeat + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function blockDescription($params, $content, $template, &$repeat) + { + if ($content !== null) { + // Clean-up whitespace and newlines + $content = preg_replace('/\s+/', ' ', trim($content)); + + Yii::$app->getView()->registerMetaTag(['name' => 'description', + 'content' => $content], + 'description'); + } + } + + /** + * Smarty function plugin + * Usage is the following: + * + * {registerJsFile url='http://maps.google.com/maps/api/js?sensor=false' position='POS_END'} + * + * Supported attributes: url, key, depends, position and valid HTML attributes for the script tag. + * Refer to Yii documentation for details. + * The position attribute is passed as text without the class prefix. + * Default is 'POS_END'. + * + * @param $params + * @param \Smarty_Internal_Template $template + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function functionRegisterJsFile($params, $template) + { + if (!isset($params['url'])) { + trigger_error("registerJsFile: missing 'url' parameter"); + } + + $url = ArrayHelper::remove($params, 'url'); + $key = ArrayHelper::remove($params, 'key', null); + $depends = ArrayHelper::remove($params, 'depends', null); + if (isset($params['position'])) + $params['position'] = $this->getViewConstVal($params['position'], View::POS_END); + + Yii::$app->getView()->registerJsFile($url, $depends, $params, $key); + } + + /** + * Smarty block function plugin + * Usage is the following: + * + * {registerJs key='show' position='POS_LOAD'} + * $("span.show").replaceWith('
'); + * {/registerJs} + * + * Supported attributes: key, position. Refer to Yii documentation for details. + * The position attribute is passed as text without the class prefix. + * Default is 'POS_READY'. + * + * @param $params + * @param $content + * @param \Smarty_Internal_Template $template + * @param $repeat + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function blockJavaScript($params, $content, $template, &$repeat) + { + if ($content !== null) { + $key = isset($params['key']) ? $params['key'] : null; + $position = isset($params['position']) ? $params['position'] : null; + + Yii::$app->getView()->registerJs($content, + $this->getViewConstVal($position, View::POS_READY), + $key); + } + } + + /** + * Smarty function plugin + * Usage is the following: + * + * {registerCssFile url='@assets/css/normalizer.css'} + * + * Supported attributes: url, key, depends and valid HTML attributes for the link tag. + * Refer to Yii documentation for details. + * + * @param $params + * @param \Smarty_Internal_Template $template + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function functionRegisterCssFile($params, $template) + { + if (!isset($params['url'])) { + trigger_error("registerCssFile: missing 'url' parameter"); + } + + $url = ArrayHelper::remove($params, 'url'); + $key = ArrayHelper::remove($params, 'key', null); + $depends = ArrayHelper::remove($params, 'depends', null); + + Yii::$app->getView()->registerCssFile($url, $depends, $params, $key); + } + + /** + * Smarty block function plugin + * Usage is the following: + * + * {registerCss} + * div.header { + * background-color: #3366bd; + * color: white; + * } + * {/registerCss} + * + * Supported attributes: key and valid HTML attributes for the style tag. + * Refer to Yii documentation for details. + * + * @param $params + * @param $content + * @param \Smarty_Internal_Template $template + * @param $repeat + * @return string + * @note Even though this method is public it should not be called directly. + */ + public function blockCss($params, $content, $template, &$repeat) + { + if ($content !== null) { + $key = isset($params['key']) ? $params['key'] : null; + + Yii::$app->getView()->registerCss($content, $params, $key); + } + } + + /** + * Helper function to convert a textual constant identifier to a View class + * integer constant value. + * + * @param string $string Constant identifier name + * @param integer $default Default value + * @return mixed + */ + protected function getViewConstVal($string, $default) + { + $val = @constant('yii\web\View::' . $string); + return isset($val) ? $val : $default; + } +} \ No newline at end of file diff --git a/extensions/smarty/ViewRenderer.php b/extensions/smarty/ViewRenderer.php index 7b70963..5f79014 100644 --- a/extensions/smarty/ViewRenderer.php +++ b/extensions/smarty/ViewRenderer.php @@ -9,14 +9,17 @@ namespace yii\smarty; use Yii; use Smarty; -use yii\base\View; +use yii\web\View; +use yii\base\Widget; use yii\base\ViewRenderer as BaseViewRenderer; -use yii\helpers\Url; +use yii\base\InvalidConfigException; +use yii\helpers\ArrayHelper; /** * SmartyViewRenderer allows you to use Smarty templates in views. * * @author Alexander Makarov + * @author Henrik Maier * @since 2.0 */ class ViewRenderer extends BaseViewRenderer @@ -29,45 +32,202 @@ class ViewRenderer extends BaseViewRenderer * @var string the directory or path alias pointing to where Smarty compiled templates will be stored. */ public $compilePath = '@runtime/Smarty/compile'; + + /** + * @var array Add additional directories to Smarty's search path for plugins. + */ + public $pluginDirs = []; + /** + * @var array Class imports similar to the use tag + */ + public $imports = []; + /** + * @var array Widget declarations + */ + public $widgets = ['functions' => [], 'blocks' => []]; /** - * @var Smarty + * @var Smarty The Smarty object used for rendering */ - public $smarty; + protected $smarty; + /** + * @var array additional Smarty options + * @see http://www.smarty.net/docs/en/api.variables.tpl + */ + public $options = []; + + /** + * @var string extension class name + */ + public $extensionClass = '\yii\smarty\Extension'; + + /** + * Instantiates and configures the Smarty object. + */ public function init() { $this->smarty = new Smarty(); $this->smarty->setCompileDir(Yii::getAlias($this->compilePath)); $this->smarty->setCacheDir(Yii::getAlias($this->cachePath)); - $this->smarty->registerPlugin('function', 'path', [$this, 'smarty_function_path']); + foreach ($this->options as $key => $value) { + $this->smarty->$key = $value; + } + + $this->smarty->setTemplateDir([ + dirname(Yii::$app->getView()->getViewFile()), + Yii::$app->getViewPath(), + ]); + + // Add additional plugin dirs from configuration array, apply Yii's dir convention + foreach ($this->pluginDirs as &$dir) { + $dir = $this->resolveTemplateDir($dir); + } + $this->smarty->addPluginsDir($this->pluginDirs); + + if (isset($this->imports)) { + foreach(($this->imports) as $tag => $class) { + $this->smarty->registerClass($tag, $class); + } + } + // Register block widgets specified in configuration array + if (isset($this->widgets['blocks'])) { + foreach(($this->widgets['blocks']) as $tag => $class) { + $this->smarty->registerPlugin('block', $tag, [$this, '_widget_block__' . $tag]); + $this->smarty->registerClass($tag, $class); + } + } + // Register function widgets specified in configuration array + if (isset($this->widgets['functions'])) { + foreach(($this->widgets['functions']) as $tag => $class) { + $this->smarty->registerPlugin('function', $tag, [$this, '_widget_func__' . $tag]); + $this->smarty->registerClass($tag, $class); + } + } + + new $this->extensionClass($this, $this->smarty); + + $this->smarty->default_template_handler_func = [$this, 'aliasHandler']; } /** - * Smarty template function to get a path for using in links - * - * Usage is the following: + * The directory can be specified in Yii's standard convention + * using @, // and / prefixes or no prefix for view relative directories. * - * {path route='blog/view' alias=$post.alias user=$user.id} - * - * where route is Yii route and the rest of parameters are passed as is. + * @param string $dir directory name to be resolved + * @return string the resolved directory name + */ + protected function resolveTemplateDir($dir) + { + if (strncmp($dir, '@', 1) === 0) { + // e.g. "@app/views/dir" + $dir = Yii::getAlias($dir); + } elseif (strncmp($dir, '//', 2) === 0) { + // e.g. "//layouts/dir" + $dir = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($dir, '/'); + } elseif (strncmp($dir, '/', 1) === 0) { + // e.g. "/site/dir" + if (Yii::$app->controller !== null) { + $dir = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($dir, '/'); + } else { + // No controller, what to do? + } + } else { + // relative to view file + $dir = dirname(Yii::$app->getView()->getViewFile()) . DIRECTORY_SEPARATOR . $dir; + } + + return $dir; + } + + /** + * Mechanism to pass a widget's tag name to the callback function. * - * @param $params - * @param \Smarty_Internal_Template $template + * Using a magic function call would not be necessary if Smarty would + * support closures. Smarty closure support is announced for 3.2, + * until its release magic function calls are used to pass the + * tag name to the callback. * + * @param string $method + * @param array $args + * @throws InvalidConfigException + * @throws \BadMethodCallException * @return string */ - public function smarty_function_path($params, \Smarty_Internal_Template $template) + public function __call($method, $args) { - if (!isset($params['route'])) { - trigger_error("path: missing 'route' parameter"); + $methodInfo = explode('__', $method); + if (count($methodInfo) === 2) { + $alias = $methodInfo[1]; + if (isset($this->widgets['functions'][$alias])) { + if (($methodInfo[0] === '_widget_func') && (count($args) === 2)) { + return $this->widgetFunction($this->widgets['functions'][$alias], $args[0], $args[1]); + } + } elseif (isset($this->widgets['blocks'][$alias])) { + if (($methodInfo[0] === '_widget_block') && (count($args) === 4)) { + return $this->widgetBlock($this->widgets['blocks'][$alias], $args[0], $args[1], $args[2], $args[3]); + } + } else { + throw new InvalidConfigException('Widget "' . $alias . '" not declared.'); + } } - array_unshift($params, $params['route']) ; - unset($params['route']); + throw new \BadMethodCallException('Method does not exist: ' . $method); + } + + /** + * Smarty plugin callback function to support widget as Smarty blocks. + * This function is not called directly by Smarty but through a + * magic __call wrapper. + * + * Example usage is the following: + * + * {ActiveForm assign='form' id='login-form'} + * {$form->field($model, 'username')} + * {$form->field($model, 'password')->passwordInput()} + *
+ * + *
+ * {/ActiveForm} + */ + private function widgetBlock($class, $params, $content, \Smarty_Internal_Template $template, &$repeat) + { + // Check if this is the opening ($content is null) or closing tag. + if ($content === null) { + $params['class'] = $class; + // Figure out where to put the result of the widget call, if any + $assign = ArrayHelper::remove($params, 'assign', false); + ob_start(); + ob_implicit_flush(false); + $widget = Yii::createObject($params); + Widget::$stack[] = $widget; + if ($assign) { + $template->assign($assign, $widget); + } + } else { + $widget = array_pop(Widget::$stack); + echo $content; + $out = $widget->run(); + return ob_get_clean() . $out; + } + } - return Url::to($params); + /** + * Smarty plugin callback function to support widgets as Smarty functions. + * This function is not called directly by Smarty but through a + * magic __call wrapper. + * + * Example usage is the following: + * + * {GridView dataProvider=$provider} + * + */ + private function widgetFunction($class, $params, \Smarty_Internal_Template $template) + { + $repeat = false; + $this->widgetBlock($class, $params, null, $template, $repeat); // $widget->init(...) + return $this->widgetBlock($class, $params, '', $template, $repeat); // $widget->run() } /** @@ -79,17 +239,35 @@ class ViewRenderer extends BaseViewRenderer * @param View $view the view object used for rendering the file. * @param string $file the view file. * @param array $params the parameters to be passed to the view file. - * * @return string the rendering result */ public function render($view, $file, $params) { /* @var $template \Smarty_Internal_Template */ - $template = $this->smarty->createTemplate($file, null, null, empty($params) ? null : $params, true); + $template = $this->smarty->createTemplate($file, null, null, empty($params) ? null : $params, false); + + // Make Yii params available as smarty config variables + $template->config_vars = Yii::$app->params; $template->assign('app', \Yii::$app); $template->assign('this', $view); return $template->fetch(); } + + /** + * Resolves Yii alias into file path + * + * @param string $type + * @param string $name + * @param string $content + * @param string $modified + * @param Smarty $smarty + * @return bool|string path to file or false if it's not found + */ + public function aliasHandler($type, $name, &$content, &$modified, Smarty $smarty) + { + $file = Yii::getAlias($name); + return is_file($file) ? $file : false; + } } diff --git a/tests/unit/extensions/smarty/ViewRendererTest.php b/tests/unit/extensions/smarty/ViewRendererTest.php index 3b73ab9..5fe1137 100644 --- a/tests/unit/extensions/smarty/ViewRendererTest.php +++ b/tests/unit/extensions/smarty/ViewRendererTest.php @@ -10,6 +10,7 @@ namespace yiiunit\extensions\smarty; use yii\web\AssetManager; use yii\web\View; use Yii; +use yiiunit\data\base\Singer; use yiiunit\TestCase; /** @@ -41,6 +42,47 @@ class ViewRendererTest extends TestCase $this->assertEquals('test view Hello World!.', $content); } + public function testLayoutAssets() + { + $view = $this->mockView(); + $content = $view->renderFile('@yiiunit/extensions/smarty/views/layout.tpl'); + + $this->assertEquals(1, preg_match('#\s*#', $content), 'Content does not contain the jquery js:' . $content); + } + + + public function testChangeTitle() + { + $view = $this->mockView(); + $view->title = 'Original title'; + + $content = $view->renderFile('@yiiunit/extensions/smarty/views/changeTitle.tpl'); + $this->assertTrue(strpos($content, 'New title') !== false, 'New title should be there:' . $content); + $this->assertFalse(strpos($content, 'Original title') !== false, 'Original title should not be there:' . $content); + } + + public function testForm() + { + $view = $this->mockView(); + $model = new Singer(); + $content = $view->renderFile('@yiiunit/extensions/smarty/views/form.tpl', ['model' => $model]); + $this->assertEquals(1, preg_match('#
.*?
#s', $content), 'Content does not contain form:' . $content); + } + + public function testInheritance() + { + $view = $this->mockView(); + $content = $view->renderFile('@yiiunit/extensions/smarty/views/extends2.tpl'); + $this->assertTrue(strpos($content, 'Hello, I\'m inheritance test!') !== false, 'Hello, I\'m inheritance test! should be there:' . $content); + $this->assertTrue(strpos($content, 'extends2 block') !== false, 'extends2 block should be there:' . $content); + $this->assertFalse(strpos($content, 'extends1 block') !== false, 'extends1 block should not be there:' . $content); + + $content = $view->renderFile('@yiiunit/extensions/smarty/views/extends3.tpl'); + $this->assertTrue(strpos($content, 'Hello, I\'m inheritance test!') !== false, 'Hello, I\'m inheritance test! should be there:' . $content); + $this->assertTrue(strpos($content, 'extends3 block') !== false, 'extends3 block should be there:' . $content); + $this->assertFalse(strpos($content, 'extends1 block') !== false, 'extends1 block should not be there:' . $content); + } + /** * @return View */ @@ -50,6 +92,9 @@ class ViewRendererTest extends TestCase 'renderers' => [ 'tpl' => [ 'class' => 'yii\smarty\ViewRenderer', + 'options' => [ + 'force_compile' => true, // always recompile templates, don't do it in production + ], ], ], 'assetManager' => $this->mockAssetManager(), diff --git a/tests/unit/extensions/smarty/views/changeTitle.tpl b/tests/unit/extensions/smarty/views/changeTitle.tpl new file mode 100644 index 0000000..0b34602 --- /dev/null +++ b/tests/unit/extensions/smarty/views/changeTitle.tpl @@ -0,0 +1,3 @@ +{set title='New title'} + +{$this->title} \ No newline at end of file diff --git a/tests/unit/extensions/smarty/views/extends1.tpl b/tests/unit/extensions/smarty/views/extends1.tpl new file mode 100644 index 0000000..a5a386a --- /dev/null +++ b/tests/unit/extensions/smarty/views/extends1.tpl @@ -0,0 +1,5 @@ +Hello, I'm inheritance test! + +{block name=test} +extends1 block +{/block} \ No newline at end of file diff --git a/tests/unit/extensions/smarty/views/extends2.tpl b/tests/unit/extensions/smarty/views/extends2.tpl new file mode 100644 index 0000000..53d95a3 --- /dev/null +++ b/tests/unit/extensions/smarty/views/extends2.tpl @@ -0,0 +1,5 @@ +{extends file="@yiiunit/extensions/smarty/views/extends1.tpl"} + +{block name=test} +extends2 block +{/block} \ No newline at end of file diff --git a/tests/unit/extensions/smarty/views/extends3.tpl b/tests/unit/extensions/smarty/views/extends3.tpl new file mode 100644 index 0000000..5296d8c --- /dev/null +++ b/tests/unit/extensions/smarty/views/extends3.tpl @@ -0,0 +1,5 @@ +{extends file="@yiiunit/extensions/smarty/views/extends1.tpl"} + +{block name=test} +extends3 block +{/block} \ No newline at end of file diff --git a/tests/unit/extensions/smarty/views/form.tpl b/tests/unit/extensions/smarty/views/form.tpl new file mode 100644 index 0000000..91e7342 --- /dev/null +++ b/tests/unit/extensions/smarty/views/form.tpl @@ -0,0 +1,9 @@ +{use class='yii\widgets\ActiveForm' type='block'} +{ActiveForm assign='form' id='login-form' action='/form-handler' options=['class' => 'form-horizontal']} + {$form->field($model, 'firstName')} +
+
+ +
+
+{/ActiveForm} \ No newline at end of file diff --git a/tests/unit/extensions/smarty/views/layout.tpl b/tests/unit/extensions/smarty/views/layout.tpl new file mode 100644 index 0000000..4d07e82 --- /dev/null +++ b/tests/unit/extensions/smarty/views/layout.tpl @@ -0,0 +1,16 @@ +{use class="yii\web\JqueryAsset"} +{JqueryAsset::register($this)|void} +{$this->beginPage()} + + + + + {$this->title|escape} + {$this->head()} + + +{$this->beginBody()} + body +{$this->endBody()} + +{$this->endPage()} \ No newline at end of file