smarty作为view时使用Zend_Form出现方法未定义的问题原因及解决
本文由 kalon 于 2008-11-28 23:00:29 发布. ZendFramework
这个问题是phpchina上的一个网友提出来的,原帖为 http://www.phpchina.com/bbs/thread-89238-1-1.html
正好这段时间一直都在研究Zend_Form模块的代码,正好也研究了一下这个问题,最终发现了其中的奥秘,同时也发现了Zend_Form设计上的一个小错误。
问题产生的背景:
首先看看我们是如何以smarty作为Zend_View的:
Zend_View_Interface的声明如下:
- interface Zend_View_Interface
- {
- /**
- * Return the template engine object, if any
- *
- * If using a third-party template engine, such as Smarty, patTemplate,
- * phplib, etc, return the template engine object. Useful for calling
- * methods on these objects, such as for setting filters, modifiers, etc.
- *
- * @return mixed
- */
- public function getEngine();
- /**
- * Set the path to find the view script used by render()
- *
- * @param string|array The directory (-ies) to set as the path. Note that
- * the concrete view implentation may not necessarily support multiple
- * directories.
- * @return void
- */
- public function setScriptPath($path);
- /**
- * Retrieve all view script paths
- *
- * @return array
- */
- public function getScriptPaths();
- /**
- * Set a base path to all view resources
- *
- * @param string $path
- * @param string $classPrefix
- * @return void
- */
- public function setBasePath($path, $classPrefix = 'Zend_View');
- /**
- * Add an additional path to view resources
- *
- * @param string $path
- * @param string $classPrefix
- * @return void
- */
- public function addBasePath($path, $classPrefix = 'Zend_View');
- /**
- * Assign a variable to the view
- *
- * @param string $key The variable name.
- * @param mixed $val The variable value.
- * @return void
- */
- public function __set($key, $val);
- /**
- * Allows testing with empty() and isset() to work
- *
- * @param string $key
- * @return boolean
- */
- public function __isset($key);
- /**
- * Allows unset() on object properties to work
- *
- * @param string $key
- * @return void
- */
- public function __unset($key);
- /**
- * Assign variables to the view script via differing strategies.
- *
- * Suggested implementation is to allow setting a specific key to the
- * specified value, OR passing an array of key => value pairs to set en
- * masse.
- *
- * @see __set()
- * @param string|array $spec The assignment strategy to use (key or array of key
- * => value pairs)
- * @param mixed $value (Optional) If assigning a named variable, use this
- * as the value.
- * @return void
- */
- public function assign($spec, $value = null);
- /**
- * Clear all assigned variables
- *
- * Clears all variables assigned to Zend_View either via {@link assign()} or
- * property overloading ({@link __get()}/{@link __set()}).
- *
- * @return void
- */
- public function clearVars();
- /**
- * Processes a view script and returns the output.
- *
- * @param string $name The script script name to process.
- * @return string The script output.
- */
- public function render($name);
- }
根据里矢替换原则,只要我们实现这个接口,要可以用smarty作为view,那么这重情况适配器模式就发挥了很大的作用(这个模式可以让我们达到“指鹿为马”这个看似荒唐的效果)。适配器模式分为类的适配器和对象的适配器,在这里我们是用对象的适配器模式将smarty变成view,适配器代码如下:
- class Zend_viewSmarty_Adapter implements Zend_View_Interface
- {
- /**
- * Smarty object
- * @var Smarty
- */
- protected $_smarty;
- /**
- * Constructor.
- *
- * @param array $extraParams
- * @return void
- */
- public function __construct($extraParams = array())
- {
- $this->_smarty = new Smarty;
- foreach ($extraParams as $key => $value) {
- $this->_smarty->$key = $value;
- }
- }
- /**
- * Return the template engine object.
- *
- * @return Smarty
- */
- public function getEngine()
- {
- return $this->_smarty;
- }
- /**
- * Sets the base directory path to templates.
- *
- * @param string $path
- * @return Custom_View_Smarty
- */
- public function setBasePath($path, $classPrefix = 'Zend_View')
- {
- $path = rtrim($path, '///') . DIRECTORY_SEPARATOR;
- $parentDir = dirname($path) . DIRECTORY_SEPARATOR;
- $this->setTemplateDir($path . 'scripts');
- $this->setCompileDir($parentDir . '_templates_c');
- $this->setCacheDir($parentDir . '_cache');
- return $this;
- }
- /**
- * Alias of setBasePath() method.
- *
- * @param string $path
- * @return Custom_View_Smarty
- */
- public function addBasePath($path, $classPrefix = 'Zend_View')
- {
- $this->setBasePath($path);
- return $this;
- }
- /**
- * Sets the directory path to templates.
- *
- * @param string $path
- * @return Custom_View_Smarty
- */
- public function setTemplateDir($path)
- {
- if (is_dir($path) && is_readable($path)) {
- $this->_smarty->template_dir = $path;
- return $this;
- }
- throw new Exception('Invalid path provided');
- }
- /**
- * Sets the directory path to compiled templates.
- *
- * @param string $path
- * @return Custom_View_Smarty
- */
- public function setCompileDir($path)
- {
- if (is_dir($path) && is_writable($path)) {
- $this->_smarty->compile_dir = $path;
- return $this;
- }
- throw new Exception('Invalid path provided');
- }
- /**
- * Sets the directory path to cache templates.
- *
- * @param string $path
- * @return Custom_View_Smarty
- */
- public function setCacheDir($path)
- {
- if (is_dir($path) && is_writable($path)) {
- $this->_smarty->cache_dir = $path;
- return $this;
- }
- throw new Exception('Invalid path provided');
- }
- /**
- * Alias of setTemplateDir() method.
- *
- * @param string $path
- * @return Custom_View_Smarty
- */
- public function setScriptPath($path)
- {
- $this->setTemplateDir($path);
- return $this;
- }
- /**
- * Returns an array of the directory path to templates.
- *
- * @return array
- */
- public function getScriptPaths()
- {
- return array($this->_smarty->template_dir);
- }
- /**
- * Assign a variable to the template
- *
- * @param string $key The variable name.
- * @param mixed $val The variable value.
- * @return void
- */
- public function __set($key, $val)
- {
- $this->_smarty->assign($key, $val);
- }
- /**
- * Retrieve an assigned variable
- *
- * @param string $key The variable name.
- * @return mixed The variable value.
- */
- public function __get($key)
- {
- return $this->_smarty->get_template_vars($key);
- }
- /**
- * Allows testing with empty() and isset() to work
- *
- * @param string $key
- * @return boolean
- */
- public function __isset($key)
- {
- return (null !== $this->_smarty->get_template_vars($key));
- }
- /**
- * Allows unset() on object properties to work
- *
- * @param string $key
- * @return void
- */
- public function __unset($key)
- {
- $this->_smarty->clear_assign($key);
- }
- /**
- * Assign variables to the template
- *
- * Allows setting a specific key to the specified value, OR passing an array
- * of key => value pairs to set en masse.
- *
- * @see __set()
- * @param string|array $spec The assignment strategy to use (key or array of key
- * => value pairs)
- * @param mixed $value (Optional) If assigning a named variable, use this
- * as the value.
- * @return void
- */
- public function assign($spec, $value = null)
- {
- if (is_array($spec)) {
- $this->_smarty->assign($spec);
- return;
- }
- $this->_smarty->assign($spec, $value);
- }
- /**
- * Clear all assigned variables
- *
- * Clears all variables assigned to Zend_View either via {@link assign()} or
- * property overloading ({@link __get()}/{@link __set()}).
- *
- * @return void
- */
- public function clearVars()
- {
- $this->_smarty->clear_all_assign();
- }
- /**
- * Processes a template and returns the output.
- *
- * @param string $name The template to process.
- * @return string The output.
- */
- public function render($name)
- {
- return $this->_smarty->fetch($name);
- }
- }
我们的适配器类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中定义:
这些代码说明了两个问题:
- 无论如何设置,$view是一个Zend_View_Interface类型
- 如果我们没有手动在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作为一种解释性的脚本语言,没有编译检查的过程,只能在编写代码的时候多加留意,并且对编写好的代码做充分的测试。
- $myZendForm->render(new Zend_View());
- class Zend_Form_Element implements Zend_Validate_Interface
- {
- /**
- * @var Zend_View_Interface
- */
- protected $_view;
- //................
- /**
- * Retrieve view object
- *
- * Retrieves from ViewRenderer if none previously set.
- *
- * @return null|Zend_View_Interface
- */
- public function getView()
- {
- if (null === $this->_view) {
- require_once 'Zend/Controller/Action/HelperBroker.php';
- $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer');
- $this->setView($viewRenderer->view);
- }
- return $this->_view;
- }
- //.............
- /**
- * Set view object
- *
- * @param Zend_View_Interface $view
- * @return Zend_Form_Element
- */
- public function setView(Zend_View_Interface $view = null)
- {
- $this->_view = $view;
- return $this;
- }
- //.....
- }
- public function __call($name, $args)
- {
- // is the helper already loaded?
- $helper = $this->getHelper($name);
- // call the helper method
- return call_user_func_array(
- array($helper, $name),
- $args
- );
- }
- /**
- * Render an element using a view helper
- *
- * Determine view helper from 'viewHelper' option, or, if none set, from
- * the element type. Then call as
- * helper($element->getName(), $element->getValue(), $element->getAttribs())
- *
- * @param string $content
- * @return string
- * @throws Zend_Form_Decorator_Exception if element or view are not registered
- */
- public function render($content)
- {
- $element = $this->getElement();
- $view = $element->getView();
- if (null === $view) {
- require_once 'Zend/Form/Decorator/Exception.php';
- throw new Zend_Form_Decorator_Exception('ViewHelper decorator cannot render without a registered view object');
- }
- if (method_exists($element, 'getMultiOptions')) {
- $element->getMultiOptions();
- }
- $helper = $this->getHelper();
- $separator = $this->getSeparator();
- $value = $this->getValue($element);
- $attribs = $this->getElementAttribs();
- $name = $element->getFullyQualifiedName();
- $id = $element->getId();
- $attribs['id'] = $id;
- $elementContent = $view->$helper($name, $value, $attribs, $element->options);
- switch ($this->getPlacement()) {
- case self::APPEND:
- return $content . $separator . $elementContent;
- case self::PREPEND:
- return $elementContent . $separator . $content;
- default:
- return $elementContent;
- }
- }
- $myZendForm->render();