網站製作學習誌

記錄學習製作網站的一切

將 Smarty 3 整合到 Zend Framework 1.11 中

在之前的「 Smarty 2 整合到 Zend Framework 1.10 的重點整理」一文中,我們已經將 Smarty 2 整合到 Zend Framework 裡了,但是我後來發現在實戰應用裡,會因為 Smarty 預設的錯誤處理而使得 Zend Framework 沒辦法正確顯示 Exception !

終於在經過多次的嘗試下,我找到了一個更佳的解法,以下是重點整理。

準備

  1. 先依照「建立 Portable 的 ZF 專案」一文準備一個新專案,然後按照「讓 Zend Framework 把 Error 當做 Exception 處理」一文將錯誤處理的部份設定好。

  2. 接著下載 Smarty 最新的版本 (本文使用 3.0.7 版) ,並在解壓縮後將 libs 更名為 Smarty3 再放到專案的 library 目錄下。

註:如果暫時不想使用 Smarty 3 的話,也可以下載 Smarty 2 最新的版本,然後按照上面的步驟進行,但要將 libs 目錄改名為 Smarty2 。

整合

與「 Smarty 2 整合到 Zend Framework 1.10 的重點整理」的原理差不多,我們要先建立 Plugin 和自訂的 View ,程式如下:

My_Controller_Plugin_View_Smarty 類別

接下來請新建 library/My/Controller/Plugin/View/Smarty.php 這個檔案:

註:跟前一個作法不同,我在 Plugin 下有多了一個 View 目錄,請特別注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
/**
 * @see Zend_Controller_Plugin_Abstract
 */
require_once 'Zend/Controller/Plugin/Abstract.php';
/**
 * 動態處理 Smarty 的路徑屬性
 *
 */
class My_Controller_Plugin_View_Smarty extends Zend_Controller_Plugin_Abstract
{
    /**
     * 動態切換 template_dir 及 compile_dir
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        // Set ViewRenderer
        $frontController = Zend_Controller_Front::getInstance();
        $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');
        $smarty = $viewRenderer->view->getEngine();
        // 處理 Smarty 路徑
        if ($frontController->getDefaultModule() != $request->getModuleName()) {
            $smarty->compile_dir .= '/' . $this->getRequest()->getModuleName();
            if (!file_exists($smarty->compile_dir)) {
                mkdir($smarty->compile_dir);
            }
        }
    }
}

My_View_Smarty

接著再建立 library/My/View/Smarty.php 這個檔案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
<?php
defined('SMARTY_VERSION') || define('SMARTY_VERSION', 3);
/**
 * @see Smarty
 */
require_once 'Smarty' . SMARTY_VERSION . '/Smarty.class.php';
/**
 * @see Zend_View_Abstract
 */
require_once 'Zend/View/Abstract.php';
/**
 * 處理樣版
 *
 */
class My_View_Smarty extends Zend_View_Abstract
{
    /**
     * Smarty 物件
     *
     * @var Smarty
     */
    protected $_smarty = null;
    /**
     * 建構子
     *
     * 處理 Smarty 相關設定
     *
     * @param array $config Configuration data
     */
    public function __construct($config = array())
    {
        parent::__construct($config);
        $this->_smarty = new Smarty();
        if (array_key_exists('params', $config)) {
            foreach ($config['params'] as $key => $value) {
                if ('plugins_dir' === $key) {
                    $this->_addtPluginDir($value);
                } else {
                    if (property_exists('Smarty', $key)) {
                        $this->_smarty->$key = $value;
                    }
                }
            }
        }
        $this->_addtPluginDir(dirname(__FILE__) . '/Smarty/plugins');
        $this->_smarty->assign('this', $this);
    }
    /**
     * 新增 Plugin 搜尋路徑
     *
     * @param string $pluginPath 
     */
    protected function _addtPluginDir($pluginPath)
    {
        if (3 === SMARTY_VERSION) {
            $this->_smarty->addPluginsDir($pluginPath);
        } else {
            $this->_smarty->plugins_dir[] = $pluginPath;
        }
    }
    /**
     * 回傳 Smarty 物件
     *
     * @return Smarty
     */
    public function getEngine()
    {
        return $this->_smarty;
    }
    /**
     * 設定 Smarty 屬性
     *
     * @param string $key
     * @param mixed $value
     */
    public function setParam($key, $value)
    {
        $this->_smarty->$key = $value;
    }
    /**
     * 設定樣版變數
     *
     * @param string $key 樣版變數名稱
     * @param mixed $value 樣版變數值
     */
    public function __set($key, $value)
    {
        parent::__set($key, $value);
        $this->_smarty->assign($key, $value);
    }
    /**
     * 取得樣版變數
     *
     * @param string $key 樣版變數名稱
     * @return mixed 樣版變數值
     */
    public function __get($key)
    {
        return $this->_smarty->getTemplateVars($key);
    }
    /**
     * 檢查樣版變數是否存在
     *
     * @param string $key 樣版變數名稱
     * @return boolean
     */
    public function __isset($key)
    {
        return null === $this->_smarty->getTemplateVars($key);
    }
    /**
     * 移除樣版變數
     *
     * @param string $key 樣版變數名稱
     */
    public function __unset($key)
    {
        parent::__unset($key);
        $this->_smarty->clearAssign($key);
    }
    /**
     * 設定樣版變數
     *
     * @param string | array $var 樣版變數名稱或樣版變數陣列 (key => value)
     * @param mixed $value 樣版變數值
     */
    public function assign($var, $value = null)
    {
        if (is_array($var)) {
            foreach ($var as $key => $value) {
                parent::__set($key, $value);
            }
            $this->_smarty->assign($var);
            return;
        }
        $this->__set($var, $value);
    }
    /**
     * 移除所有樣版變數
     */
    public function clearVars()
    {
        parent::clearVars();
        $this->_smarty->clearAllAssign();
    }
    /**
     * Extension of the abstract parent class method
     */
    protected function _run()
    {
        $oldErrorHandler = set_error_handler(array($this, 'emptyErrorHandler'));
        $file = func_get_arg(0);
        echo $this->_smarty->fetch($file);
        restore_error_handler();
    }
    /**
     *
     */
    public function emptyErrorHandler()
    {
    }
}

不過與前一個作法不一樣的地方是,我改在 _run() 這個內建的抽象方法去呼叫 Smarty 的 fetch() 方法,如此一來我們就可以不需要特別去處理 View Script 的部份;同時也解決掉了前一個作法中,當沒有 View Script 時,畫面會一片空白的問題。

而且在 _run() 這個方法裡,我把原來在 index.php (入口) 裡的 errorExceptionHandler 關掉了,這樣才不會讓 Smarty 一直跑出樣版變數未定義的錯誤。

另外原來 Zend_View 的幾個魔術方法 (例如 __set 、 __get 等) ,除了轉手給 Smarty 處理外,我還保留呼叫 parent 的同名方法;這邊主要是為了讓其他因為特別因素而沒有用到 Smarty 的套件 (像是 Zend_Form ) , Zend_View 還是能保留原來的動作機制。

還有就是我也定義了一個 SMARTY_VERSION 常數,讓大家可以切換 Smarty 版本;只要在 index.php (入口) 最上方加入以下程式碼即可切換 Smarty 的版本:

1
defined('SMARTY_VERSION') || define('SMARTY_VERSION', 3);

接下來就要讓專案可以載入以上兩個類別了。

My_Application_Resource_View

在前一個作法中,我們是改寫 application/Bootstrap.php 以載入上面操作 Smarty 用的兩個類別。但是這次我改用 Application 的 View Resource 來載入,這樣就不需要去更動 Bootstrap.php ,也可以讓我們的程式碼更具移植性。

所以最後我們要再建立 My/Application/Resource/View.php 這個檔案,程式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<?php
/**
 * @see Zend_Application_Resource_View
 */
require_once 'Zend/Application/Resource/View.php';
/**
 * 處理視圖引擎
 *
 */
class My_Application_Resource_View extends Zend_Application_Resource_View
{
    /**
     * 視圖物件選項
     *
     * @var array
     */
    protected $_options = array();
    /**
     * 視圖物件
     * 
     * @var Zend_View
     */
    protected $_view = null;
    /**
     * 初始化視圖物件
     * 
     */
    public function init()
    {
        parent::init();
        $this->_setControllerPlugin();
        $this->_setViewRenderer();
        $this->_setLayout();
        return $this->getView();
    }
    /**
     * 取得引擎名稱
     *
     * @return string
     */
    protected function _getEngineName()
    {
        return isset($this->_options['engine'])
             ? ucfirst(strtolower(trim($this->_options['engine'])))
             : null;
    }
    /**
     * 設定 Controller Plugin
     *
     */
    protected function _setControllerPlugin()
    {
        $engineName = $this->_getEngineName();
        if ($engineName) {
            $pluginName = 'My_Controller_Plugin_View_' . $this->_getEngineName();
            Zend_Controller_Front::getInstance()->registerPlugin(new $pluginName());
        }
    }
    /**
     * 將 View 置入到 ViewRenderer
     *
     */
    protected function _setViewRenderer()
    {
        parent::init(); // Set View into ViewRenderer
        if (isset($this->_options['viewSuffix'])) {
            Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer')
                ->setViewSuffix($this->_options['viewSuffix']);
        }
    }
    /**
     * 將 View 置入到 Layout
     *
     */
    protected function _setLayout()
    {
        $bootstrap = $this->getBootstrap();
        if ($bootstrap->hasPluginResource('layout')) {
            $bootstrap->bootstrap('layout');
            $bootstrap->getPluginResource('layout')->getLayout()->setView($this->getView());
        }
    }
    /**
     * 取得視圖物件
     *
     * @return Zend_View
     */
    public function getView()
    {
        if (null == $this->_view) {
            $engineName = $this->_getEngineName();
            if (null === $engineName) {
                $this->_view = parent::getView();
            } else {
                $viewName = 'My_View_' . $this->_getEngineName();
                $options = $this->getOptions();
                $this->_view = new $viewName($options);
                if (isset($options['doctype'])) {
                    $this->_view->doctype()->setDoctype(strtoupper($options['doctype']));
                }
            }
        }
        return $this->_view;
    }
}

基本上 My_Application_Resource_View 做的事情都跟前一個作法裡的 Bootstrap.php 差不多,只是我將它重構得更清楚。只要在稍後做一些設定後, My_Application_Resource_View 將會取代 Zend_Application_Resource_View 來執行初始化 View 的動作。

為什麼要改用 View Resource 呢?主要原因如下:

  • 它可以幫我們取得 application.ini 中,有關 resources.view 的設定。

  • 具有移植性,可以隨著 My 套件移植到其他專案上。

  • 可以切換回預設的 Zend_View 樣版引擎。

當然只是把我們的 My 套件放在 library 下並不能發揮它的作用,我們必須告訴專案來使用這些新類別。

設定

接下來就要讓 My 套件整合到我們的專案裡了,我們只需要在 applicatin.ini 中加入以下設定即可:

1
2
3
4
5
6
7
8
Autoloadernamespaces[] = "My_" ; 向 Autoloader 註冊 My 這個 namespace
pluginPaths.My_Application_Resource_ = "My/Application/Resource" ; 註冊 Resource 的搜尋路徑
resources.view.engine                 = "smarty" ; 使用 Smarty 樣版引擎,如果不需要的話就整行移除
resources.view.params.compile_dir     = APPLICATION_PATH "/temp/compiled" ; 設定 Smarty 的 compiled 路徑
resources.view.params.left_delimiter  = "<%" ; 左邊的 delimiter
resources.view.params.right_delimiter = "%>" ; 右邊的 delimiter
resources.view.params.auto_literal    = false ; 讓 delimiter 可以接受空白
resources.view.doctype                = "XHTML1_TRANSITIONAL" ; 讓 Zend_Form 等套件使用 XHTML1 來輸出

相關說明就請參考設定後面的註解。

要特別說一下的是 pluginPaths 這個設定,它會讓 Zend Framework 以註冊的 namespace 來尋找像是 Resource 或 Plugin 等未指定 namespace 的類別。原則上越晚註冊的 namespace ,就會越先被搜尋;因此我們的 My_Application_Resource_View 才能取代原來預設的 Zend_Application_Resource_View 。

還有就是 auto_literal 這個參數,這是在 Smarty 3 才特別出現的。主要是因為 Smarty 3 預設在 delimiter 和敘述語法間是不能有空白字元的,如果我們需要空白字元的話,就要將 auto_literal 關閉。例如在 auto_literal 開啟時:

1
2
3
4
{$name} // 合法
{ $name } // 不合法
{ $name
} // 不合法

當關閉了 auto_literal 後,上面的語法就都可以被 Smarty 辨別了。

樣版

作法與「 Smarty 2 整合到 Zend Framework 1.10 的重點整理」一樣,這裡就不再詳述。

小結

經過多次的實戰,加上朋友的意見反饋,我把舊有而有問題的做法改成了目前這個新方式;當然這並不一定是最完美的解法,希望大家在測試過後也能給我一些新的建議。

Comments