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*
+{$this->beginBody()}
+ body
+{$this->endBody()}
+
#', $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()}
+
+
+