smarty作为view时使用Zend_Form出现方法未定义的问题原因及解决

smarty作为view时使用Zend_Form出现方法未定义的问题原因及解决

    这个问题是phpchina上的一个网友提出来的,原帖为 http://www.phpchina.com/bbs/thread-89238-1-1.html

    正好这段时间一直都在研究Zend_Form模块的代码,正好也研究了一下这个问题,最终发现了其中的奥秘,同时也发现了Zend_Form设计上的一个小错误。

问题产生的背景:

    首先看看我们是如何以smarty作为Zend_View的:

     Zend_View_Interface的声明如下:

PHP代码
  1. interface Zend_View_Interface  
  2. {  
  3.     /** 
  4.      * Return the template engine object, if any 
  5.      * 
  6.      * If using a third-party template engine, such as Smarty, patTemplate, 
  7.      * phplib, etc, return the template engine object. Useful for calling 
  8.      * methods on these objects, such as for setting filters, modifiers, etc. 
  9.      * 
  10.      * @return mixed 
  11.      */  
  12.     public function getEngine();  
  13.   
  14.     /** 
  15.      * Set the path to find the view script used by render() 
  16.      * 
  17.      * @param string|array The directory (-ies) to set as the path. Note that 
  18.      * the concrete view implentation may not necessarily support multiple 
  19.      * directories. 
  20.      * @return void 
  21.      */  
  22.     public function setScriptPath($path);  
  23.   
  24.     /** 
  25.      * Retrieve all view script paths 
  26.      * 
  27.      * @return array 
  28.      */  
  29.     public function getScriptPaths();  
  30.   
  31.     /** 
  32.      * Set a base path to all view resources 
  33.      * 
  34.      * @param  string $path 
  35.      * @param  string $classPrefix 
  36.      * @return void 
  37.      */  
  38.     public function setBasePath($path$classPrefix = 'Zend_View');  
  39.   
  40.     /** 
  41.      * Add an additional path to view resources 
  42.      * 
  43.      * @param  string $path 
  44.      * @param  string $classPrefix 
  45.      * @return void 
  46.      */  
  47.     public function addBasePath($path$classPrefix = 'Zend_View');  
  48.   
  49.     /** 
  50.      * Assign a variable to the view 
  51.      * 
  52.      * @param string $key The variable name. 
  53.      * @param mixed $val The variable value. 
  54.      * @return void 
  55.      */  
  56.     public function __set($key$val);  
  57.   
  58.     /** 
  59.      * Allows testing with empty() and isset() to work 
  60.      * 
  61.      * @param string $key 
  62.      * @return boolean 
  63.      */  
  64.     public function __isset($key);  
  65.   
  66.     /** 
  67.      * Allows unset() on object properties to work 
  68.      * 
  69.      * @param string $key 
  70.      * @return void 
  71.      */  
  72.     public function __unset($key);  
  73.   
  74.     /** 
  75.      * Assign variables to the view script via differing strategies. 
  76.      * 
  77.      * Suggested implementation is to allow setting a specific key to the 
  78.      * specified value, OR passing an array of key => value pairs to set en 
  79.      * masse. 
  80.      * 
  81.      * @see __set() 
  82.      * @param string|array $spec The assignment strategy to use (key or array of key 
  83.      * => value pairs) 
  84.      * @param mixed $value (Optional) If assigning a named variable, use this 
  85.      * as the value. 
  86.      * @return void 
  87.      */  
  88.     public function assign($spec$value = null);  
  89.   
  90.     /** 
  91.      * Clear all assigned variables 
  92.      * 
  93.      * Clears all variables assigned to Zend_View either via {@link assign()} or 
  94.      * property overloading ({@link __get()}/{@link __set()}). 
  95.      * 
  96.      * @return void 
  97.      */  
  98.     public function clearVars();  
  99.   
  100.     /** 
  101.      * Processes a view script and returns the output. 
  102.      * 
  103.      * @param string $name The script script name to process. 
  104.      * @return string The script output. 
  105.      */  
  106.     public function render($name);  
  107. }  

 

根据里矢替换原则,只要我们实现这个接口,要可以用smarty作为view,那么这重情况适配器模式就发挥了很大的作用(这个模式可以让我们达到“指鹿为马”这个看似荒唐的效果)。适配器模式分为类的适配器和对象的适配器,在这里我们是用对象的适配器模式将smarty变成view,适配器代码如下:

PHP代码
 
  1. class Zend_viewSmarty_Adapter implements Zend_View_Interface  
  2. {  
  3.     /** 
  4.      * Smarty object 
  5.      * @var Smarty 
  6.      */  
  7.     protected $_smarty;  
  8.       
  9.     /** 
  10.      * Constructor. 
  11.      * 
  12.      * @param array $extraParams 
  13.      * @return void 
  14.      */  
  15.     public function __construct($extraParams = array())  
  16.     {  
  17.         $this->_smarty = new Smarty;  
  18.   
  19.         foreach ($extraParams as $key => $value) {  
  20.             $this->_smarty->$key = $value;  
  21.         }  
  22.     }  
  23.       
  24.     /** 
  25.      * Return the template engine object. 
  26.      * 
  27.      * @return Smarty 
  28.      */  
  29.     public function getEngine()  
  30.     {  
  31.         return $this->_smarty;  
  32.     }  
  33.   
  34.     /** 
  35.      * Sets the base directory path to templates. 
  36.      * 
  37.      * @param   string  $path 
  38.      * @return  Custom_View_Smarty 
  39.      */  
  40.     public function setBasePath($path$classPrefix = 'Zend_View')  
  41.     {    
  42.         $path      = rtrim($path'///') . DIRECTORY_SEPARATOR;  
  43.         $parentDir = dirname($path) . DIRECTORY_SEPARATOR;  
  44.         $this->setTemplateDir($path . 'scripts');  
  45.         $this->setCompileDir($parentDir . '_templates_c');  
  46.         $this->setCacheDir($parentDir . '_cache');  
  47.         return $this;  
  48.     }  
  49.   
  50.     /** 
  51.      * Alias of setBasePath() method. 
  52.      * 
  53.      * @param   string  $path 
  54.      * @return  Custom_View_Smarty 
  55.      */  
  56.     public function addBasePath($path$classPrefix = 'Zend_View')  
  57.     {  
  58.         $this->setBasePath($path);  
  59.         return $this;  
  60.     }  
  61.   
  62.     /** 
  63.      * Sets the directory path to templates. 
  64.      * 
  65.      * @param   string  $path 
  66.      * @return  Custom_View_Smarty 
  67.      */  
  68.     public function setTemplateDir($path)  
  69.     {  
  70.         if (is_dir($path) && is_readable($path)) {  
  71.             $this->_smarty->template_dir = $path;  
  72.             return $this;  
  73.         }  
  74.         throw new Exception('Invalid path provided');  
  75.     }  
  76.   
  77.     /** 
  78.      * Sets the directory path to compiled templates. 
  79.      * 
  80.      * @param   string  $path 
  81.      * @return  Custom_View_Smarty 
  82.      */  
  83.     public function setCompileDir($path)  
  84.     {  
  85.         if (is_dir($path) && is_writable($path)) {  
  86.             $this->_smarty->compile_dir = $path;  
  87.             return $this;  
  88.         }  
  89.         throw new Exception('Invalid path provided');  
  90.     }  
  91.       
  92.     /** 
  93.      * Sets the directory path to cache templates. 
  94.      * 
  95.      * @param   string  $path 
  96.      * @return  Custom_View_Smarty 
  97.      */  
  98.     public function setCacheDir($path)  
  99.     {  
  100.         if (is_dir($path) && is_writable($path)) {  
  101.             $this->_smarty->cache_dir = $path;  
  102.             return $this;  
  103.         }  
  104.         throw new Exception('Invalid path provided');  
  105.     }  
  106.       
  107.     /** 
  108.      * Alias of setTemplateDir() method. 
  109.      * 
  110.      * @param   string  $path 
  111.      * @return  Custom_View_Smarty 
  112.      */  
  113.     public function setScriptPath($path)  
  114.     {  
  115.         $this->setTemplateDir($path);  
  116.         return $this;  
  117.     }  
  118.       
  119.     /** 
  120.      * Returns an array of the directory path to templates. 
  121.      * 
  122.      * @return  array 
  123.      */  
  124.     public function getScriptPaths()  
  125.     {  
  126.         return array($this->_smarty->template_dir);  
  127.     }  
  128.       
  129.     /** 
  130.      * Assign a variable to the template 
  131.      * 
  132.      * @param string $key The variable name. 
  133.      * @param mixed $val The variable value. 
  134.      * @return void 
  135.      */  
  136.     public function __set($key$val)  
  137.     {  
  138.         $this->_smarty->assign($key$val);  
  139.     }  
  140.       
  141.     /** 
  142.      * Retrieve an assigned variable 
  143.      * 
  144.      * @param string $key The variable name. 
  145.      * @return mixed The variable value. 
  146.      */  
  147.     public function __get($key)  
  148.     {  
  149.         return $this->_smarty->get_template_vars($key);  
  150.     }  
  151.       
  152.     /** 
  153.      * Allows testing with empty() and isset() to work 
  154.      * 
  155.      * @param string $key 
  156.      * @return boolean 
  157.      */  
  158.     public function __isset($key)  
  159.     {  
  160.         return (null !== $this->_smarty->get_template_vars($key));  
  161.     }  
  162.       
  163.     /** 
  164.      * Allows unset() on object properties to work 
  165.      * 
  166.      * @param string $key 
  167.      * @return void 
  168.      */  
  169.     public function __unset($key)  
  170.     {  
  171.         $this->_smarty->clear_assign($key);  
  172.     }  
  173.       
  174.     /** 
  175.      * Assign variables to the template 
  176.      * 
  177.      * Allows setting a specific key to the specified value, OR passing an array 
  178.      * of key => value pairs to set en masse. 
  179.      * 
  180.      * @see __set() 
  181.      * @param string|array $spec The assignment strategy to use (key or array of key 
  182.      * => value pairs) 
  183.      * @param mixed $value (Optional) If assigning a named variable, use this 
  184.      * as the value. 
  185.      * @return void 
  186.      */  
  187.     public function assign($spec$value = null)  
  188.     {  
  189.         if (is_array($spec)) {  
  190.             $this->_smarty->assign($spec);  
  191.             return;  
  192.         }  
  193.           
  194.         $this->_smarty->assign($spec$value);  
  195.     }  
  196.       
  197.     /** 
  198.      * Clear all assigned variables 
  199.      * 
  200.      * Clears all variables assigned to Zend_View either via {@link assign()} or 
  201.      * property overloading ({@link __get()}/{@link __set()}). 
  202.      *  
  203.      * @return void 
  204.      */  
  205.     public function clearVars()  
  206.     {  
  207.         $this->_smarty->clear_all_assign();  
  208.     }  
  209.       
  210.     /** 
  211.      * Processes a template and returns the output. 
  212.      *       
  213.      * @param string $name The template to process. 
  214.      * @return string The output. 
  215.      */  
  216.     public function render($name)  
  217.     {  
  218.         return $this->_smarty->fetch($name);  
  219.     }  
  220. }  

 

我们的适配器类Zend_viewSmarty_Adapter合理(注意是合理)的实现了Zend_View_Interface中要求的方法,那么现在我们的就指鹿(smarty)为马(Zend_View)了。

问题的本质原因:

    这样的适配方式是目前比较流行的一种方式,一直以来都能正常的为我们工作。但是当Zend_Form出来之后,似乎出现了一些诡异的问题。我们在最终render一个Zend_Form的时候:

 

但是当我们使用Zend_View而不是smarty做view的时候,一切都正常。最后我通过跟踪源代码,终于发现了问题所在。我们需要简单的了解Zend_From的包装过程,这里不做详细介绍,以后会写文章详细讲解。 Zend_Form其实是逐个将它所包含的Zend_Form_Element,Zend_Form_SubForm等等一层层包装起来(装饰器模式)。每个元素会用装饰器来一点点的装饰,其中有一个装饰器Zend_Form_Decorator_ViewHelper是问题的关键,这个装饰器会被每个Element使用来生成input标签,而生成这个标签则是通过Zend_View_Helper,我们先简单的看看Zend_Form_Decorator_ViewHelper中render()方法的内容:

 

 报错的地方就是以上代码的第33行,这里动态的调用了变量$view的一个方法$helper,这个helper其实是view helper的名字,比如formText,formFile等等,可以在Zend/View/Helper/目录下看到这些helper。其实在$view中,并没有以这些helper做为名字的方法,所以这会导致__call()魔术方法被触发,那么这个__call()魔术方法,在Zend_View_Abstract中定义如下:

 

    那么这个$view到底是什么呢?由代码可以看出$view是从装饰器所装饰的Element对象中获取的,我们看看它的类型描叙和accessor,这些在Zend_Form_Element中定义:

 

这些代码说明了两个问题:

  1. 无论如何设置,$view是一个Zend_View_Interface类型
  2. 如果我们没有手动在Zend_Form中没有设置$view,那么就自动从 $viewRenderer中取得$view

注意其中的第2点!!!回到本文我们所描叙的问题,我们一般都没有在Zend_Form中手动设置一个view,因为手册都说了这个一般可以不用设置,注意是“一般”不用。但是当我们使用smarty来做Zend_View的时候,就是一种特殊情况了,因为第2点说到,$view会从$viewRender中自动获取,所以这种情况,这个$view,其实就是我们自己定义的Zend_viewSmarty_Adapter的实例,然而这个类虽然也是Zend_View_Interface类型,但是他并没有实现__call方法,所以最终这个$view调用$helper这个方法的时候会报出一个方法未定义的错误。这个就是我们问题的本质原因了:

Zend_Form_Decorator_ViewHelper自动的获取了一个不合要求的Zend_View


问题的解决:

    
问题的本质原因我们已经找到了,那么解决问题就不那么困难了。

既然Zend_Form_Decorator_ViewHelper无法自动获取到期望的view,那么,我们手动提供一个正确的view给它,那么我们的问题就解决了。最简单的一种做法是:

 

提供了正确的View,那么Zend_Form_Decorator_ViewHelper就能顺利的通过View来调用view helper来生成我们需要的HTML代码。(这里设置的Zend_View并不会对我们自己定义的smarty view造成影响,这里的Zend_View只是一个临时对象,主要用它来调用view helper)


问题的引申:

    整个问题的背景,原因,解决方案都已经在前面描叙了。在这个问题的背后隐藏着一个更深的问题。回到我们之前问题的本质原因那一部分,Zend_Form_Decorator_ViewHelper的render()方法中的变量$view,Zend_Form要求对它的类型严格的要求为Zend_View_Interface.
依据里失替换原则(  如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都被带换成o2时,程序P的行为没有变化,那么类型T2是类T1的子类型。),任何Zend_View_Interface类型或则它的子类型,都可以做为这里的$view,并且程序的行为不会有所改变(运行时的多态性)。但是实际的情况却是我们用一个Zend_View_Interface的子类型Zend_viewSmarty_Adapter而使得程序出现了致命错误(因为$view调用了一个它这个类型不一定拥有的方法,破坏了多态性)。由此可见,Zend_Form这里的设计是违背了里失替换原则,这导致它对开闭原则的支持就不那么完美了(我们合理的扩展了原来的代码可是却产生了致命错误)。当然这反映了PHP5中type hinting的一个非常大的缺陷,在JAVA语言中,编译期间会对程序对里失替换原则的支持进行一定的检测,上面这类型的代码是不能通过编译的。PHP作为一种解释性的脚本语言,没有编译检查的过程,只能在编写代码的时候多加留意,并且对编写好的代码做充分的测试。


PHP代码
  1. $myZendForm->render(new Zend_View());  

PHP代码
 
  1. class Zend_Form_Element implements Zend_Validate_Interface  
  2. {  
  3.   
  4.     /** 
  5.      * @var Zend_View_Interface 
  6.      */  
  7.     protected $_view;  
  8.   
  9. //................  
  10.   
  11.     /** 
  12.      * Retrieve view object 
  13.      * 
  14.      * Retrieves from ViewRenderer if none previously set. 
  15.      *  
  16.      * @return null|Zend_View_Interface 
  17.      */  
  18.     public function getView()  
  19.     {  
  20.         if (null === $this->_view) {  
  21.             require_once 'Zend/Controller/Action/HelperBroker.php';  
  22.             $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');  
  23.             $this->setView($viewRenderer->view);  
  24.         }  
  25.         return $this->_view;  
  26.     }  
  27.   
  28. //.............  
  29.   
  30.   
  31.   
  32.     /** 
  33.      * Set view object 
  34.      *  
  35.      * @param  Zend_View_Interface $view  
  36.      * @return Zend_Form_Element 
  37.      */  
  38.     public function setView(Zend_View_Interface $view = null)  
  39.     {  
  40.         $this->_view = $view;  
  41.         return $this;  
  42.     }  
  43.   
  44. //.....  
  45.   
  46. }  

PHP代码
  1. public function __call($name$args)  
  2. {  
  3.     // is the helper already loaded?  
  4.     $helper = $this->getHelper($name);  
  5.   
  6.     // call the helper method  
  7.     return call_user_func_array(  
  8.         array($helper$name),  
  9.         $args  
  10.     );  
  11. }  
这样会触发一个view helper实现我们想要的操作。那么也就是说,代码能顺利的执行完全都依赖与$view这个对象的魔术方法__call().

PHP代码
 
  1. /** 
  2.  * Render an element using a view helper 
  3.  * 
  4.  * Determine view helper from 'viewHelper' option, or, if none set, from  
  5.  * the element type. Then call as  
  6.  * helper($element->getName(), $element->getValue(), $element->getAttribs()) 
  7.  *  
  8.  * @param  string $content 
  9.  * @return string 
  10.  * @throws Zend_Form_Decorator_Exception if element or view are not registered 
  11.  */  
  12. public function render($content)  
  13. {  
  14.     $element = $this->getElement();  
  15.   
  16.     $view = $element->getView();  
  17.     if (null === $view) {  
  18.         require_once 'Zend/Form/Decorator/Exception.php';  
  19.         throw new Zend_Form_Decorator_Exception('ViewHelper decorator cannot render without a registered view object');  
  20.     }  
  21.   
  22.     if (method_exists($element'getMultiOptions')) {  
  23.         $element->getMultiOptions();  
  24.     }  
  25.   
  26.     $helper        = $this->getHelper();  
  27.     $separator     = $this->getSeparator();  
  28.     $value         = $this->getValue($element);  
  29.     $attribs       = $this->getElementAttribs();  
  30.     $name          = $element->getFullyQualifiedName();  
  31.     $id            = $element->getId();  
  32.     $attribs['id'] = $id;  
  33.   
  34.     $elementContent = $view->$helper($name$value$attribs$element->options);  
  35.     switch ($this->getPlacement()) {  
  36.         case self::APPEND:  
  37.             return $content . $separator . $elementContent;  
  38.         case self::PREPEND:  
  39.             return $elementContent . $separator . $content;  
  40.         default:  
  41.             return $elementContent;  
  42.     }  
  43. }  

PHP代码
  1. $myZendForm->render();  
脚本会报错,提示方法formText(或则formFile,formRadio等)未定义。

Tags: PHP, Zend_Form

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值