Zend_Form 装饰器
Zend_Form 装饰器
Zend_Form 作为Zend Framework新增的组件,已经受到了一致的好评,在表单问题上有了一个灵活的解决方案。它的灵活性在于,它提供了已经被证实的对很多开发着来说的一个痛点:装饰器。这个教程的目的是分享装饰器上面得到的一些启发,同时也提供了一些解决方案:创建你自己的装饰器,并且把它们通过创造性的方法结合起来,最终输出你自己生成的表单。
?
背景当设计Zend_Form时,一个主要的目标是生成必要的标签去展示每个元素和表单本身。基本原理是把值和元数据注入到元素标签并且报告错误,这些操作经常是很乏味的,重复的工作。所有的表单方案都要解决这个问题并且做这些琐碎的事。
?
当然,现在对于生成表单标签的问题还没有一个标准的做法去解决它,他们的方法随着开发者和项目的不同差别也会很大,所以任何的解决方案都必须足够灵活以适应各种方法。
?
另一个值得注意的是Zend_Form没有绑定到Zend_View,这样的话,默认的装饰器实际利用Zend_View,这个解决方案需要足够灵活以至一个开发者选择不使用Zend_View,他们确实做到了。
?
心里揣着这些目标,解决方案自然也就呈现出来了:使用Decorator Pattern(http://en.wikipedia.org/wiki/Decorator_pattern)的原则。通过包装它来扩张这个类的功能,这就允许开发者通过调用相同的API来添加或者修改已经存在的特性。装饰器可能被其他装饰器包装,允许以一种分层的方法去扩展,装饰器本身被使用来代替原有的对象,并且他保持了同样的API;它仅仅提供了新建或者添加的方法到原作。
?
例如,设计一个窗口类,你可能创建一个窗口装饰器来显示这个窗口,并且添加装饰器来显示滚动条,窗口标题,等等;每个装饰器负责最终展示的一个方面,可能会像下面这样:
?
?
$window = new WindowScrollbarDecorator( new WindowTitleDecorator( new WindowDecorator( new Window() ) ) );
?
?
为了呈现最终的窗口:
$window->render(); 它可能会执行WindowScrollbarDecorator::render()方法,接着调用WindowTitleDecorator::render(),然后接着调用WindowDecorator::render(),它最终也会调用Window::render()方法,或者只是简单的使用对象状态,最终的结果就是一个带有标题和滚动条的窗口了。
?
装饰器是目前Zend_Form理想的解决方案,在使用它时,开发者可以选择性的决定在最终输出的时候显示什么,最终的解决方案在于可以修改的装饰器上;我们没有去装饰这个对象,而是装饰生成的内容,但是我们在装饰内容的时候使用的是元数据,他们来自元素或者表单对象,而传统的装饰器从外到内工作,Zend_Form的工作是从内到外的,分层的内容向外。
基础操作当装饰的时候,你可以把传入的内容进行前置,附加,或者替换(应该还包括),初始化的内容是一个空字符串,每个装饰器作用在在它上一个装饰器的执行结果上。
我们来看一下啊默认的装饰器是如何工作的,默认的装饰器是ViewHelper,Errors,HtmlTag和Label。默认的,ViewHelper装饰器替换所提供的所有内容,Error装饰器附加提供的内容,HtmlTag装饰器包装所提供的内容,Label装饰器前置提供的内容(除了ViewHelper以为的位置-附加,前置,还是包裹是可以配置的)想想一下执行结果,最终的执行会看起来像这样:
?
?
$label->render($htmlTag->render($errors->render($viewHelper->render(''))))
?
?
让我们一步一步的看看内容是如何构建的:
?
?
<input name="foo" id="foo" type="text" value="" /> <input name="foo" id="foo" type="text" value="" /> <div id="foo" type="text" value="" /> <div id="foo" type="text" value="" /> <div name="code">$label = $element->getDecorator('label');$label->setOption('placement', 'append');?
?
大部分装饰器都有很多的参数,你需要去读手册或者API文档去得到更完整的细节。
?
另一个简单的定制输出的办法是移除一个装饰器,加入你不想显示一个标签,你可以移除这个装饰器
?
?
$element->removeDecorator('label');?
?
?
很不幸的是,当前还没有办法在装饰器堆栈的一个特殊位置插入一个装饰器,所以假如你需要在堆栈中插入一个新的装饰器,最好的办法就是重置堆栈,例如。假如你想添加一个Description装饰器在元素后面,你可以这样做:
?
?
$element->setDescription($desc); $element->setDecorators(array( 'ViewHelper', 'Description', 'Errors', array('HtmlTag', array('tag' => 'dd')), array('Label', array('tag' => 'dt')), ));?
?
尽管addDecorator()和addDecorators() 方法存在,但通常还是要用setDecorators() 除非你需要很少的装饰器。
?
?
当添加装饰器的时候,你可以给他设置别名,这样做可以帮助你使用不同的名字来存储装饰器--可以帮你在堆栈中通过名字来检索到它,当你想添加两个以上的同样类型的装饰器的时候这是非常有用的;实际上在这种情况下,假如你没有设置别名,最后注册的同类型的装饰器会覆盖其它所有的实例!你可以通过传递一个数组作为装饰器的类型来完成别名的命名,这个数组是一个键/值对,键就是装饰器的别名,值是装饰器的类型。例如,假如你想使用两个不同的HTML标签在你的堆栈,你可以这样做:
?
?
$element->setDecorators(array( 'ViewHelper', 'Description', 'Errors', array(array('elementDiv' => 'HtmlTag'), array('tag' => 'div')), array(array('td' => 'HtmlTag'), array('tag' => 'td')), array('Label', array('tag' => 'td')), ));?
?
在上面的例子中,元素内容将被HTML div包裹,它返回的内容再被包裹进一个表格,这两个的别名分别被设置成‘elementDiv’ 和‘td’。
?
标准装饰器既然我们已经知道了如何操纵装饰器堆栈和个别的装饰器,那标准的装饰器是怎样的?
?
按钮和图片元素,我们使用下面方式:
?
?
$element->setDecorators(array( 'ViewHelper', array(array('data' => 'HtmlTag'),array('tag' => 'td', 'class' =>'element')), array(array('label' => 'HtmlTag'), array('tag' => 'td', 'placement' => 'prepend')), array(array('row' => 'HtmlTag'), array('tag' => 'tr')), ));?
?
?
表单大概会是这个样子
?
?
$form->setDecorators(array( 'FormElements', array('HtmlTag', array('tag' => 'table')), 'Form', ));?
?
?
接下来将会生成下面的输出结果
?
?
<form enctype="application/x-www-form-urlencoded" action="" method="post"> <table> <tr> <td><label for="username" name="username" id="username" value=""> </td> </tr> <tr> <td><label for="firstname" name="firstname" id="firstname" value=""> </td> </tr> <tr> <td><label for="lastname" name="lastname" id="lastname" value=""> </td> </tr> <tr> <td></td> <td name="save" id="save" value="Save"> </td> </tr> </table> </form>?
?
注意:这个不作用于显示组和子表单,还好,下面给出了如何去做的方法。
也许你会说,“非常好,但是我不想为每个元素都去设置装饰器”,好吧,我们不去那么做,这里有一系列的方法可以做到。
第一个,表单对象有个setElementDecorators() 方法,这个方法将设置所有的装饰器为当前所有的已经注册的元素(没有注册的元素后面调用它):
?
?
$form->setElementDecorators(array( 'ViewHelper', 'Errors', array(array('data' => 'HtmlTag'), array('tag' => 'td', 'class' => 'element')), array('Label', array('tag' => 'td'), array(array('row' => 'HtmlTag'), array('tag' => 'tr')), ));?
?
你只要这样做一次,然后循环整个表单,查找提交,重置,按钮和图片等元素来设置他们的装饰器。
?
另一个简单的选择是表单对象子类化,并且把这些定义成字符串传递给元素:
?
?
class My_Form_Registration extends Zend_Form { public $elementDecorators = array( 'ViewHelper', 'Errors', array(array('data' => 'HtmlTag'), array('tag' => 'td', 'class' => 'element')), array('Label', array('tag' => 'td'), array(array('row' => 'HtmlTag'), array('tag' => 'tr')), ); public $buttonDecorators = array( 'ViewHelper', array(array('data' => 'HtmlTag'), array('tag' => 'td', 'class' => 'element')), array(array('label' => 'HtmlTag'), array('tag' => 'td', 'placement' => 'prepend')), array(array('row' => 'HtmlTag'), array('tag' => 'tr')), ); public function init() { $this->addElement('text', 'username', array( 'decorators' => $this->elementDecorators, 'label => 'Username:', ); $this->addElement('text', 'firstname', array( 'decorators' => $this->elementDecorators, 'label => 'First Name:', ); $this->addElement('text', 'lastname', array( 'decorators' => $this->elementDecorators, 'label => 'Last Name:', ); $this->addElement('submit', 'save', array( 'decorators' => $this->buttonDecorators, 'label => 'Save', ); } public function loadDefaultDecorators() { $this->setDecorators(array( 'FormElements', array('HtmlTag', array('tag' => 'table')), 'Form', )); }}?
?
上面的办法的优点在于阻止了加载默认的装饰器并即时的使用了提供的装饰器。它还有一个优势在于你可以在一个位置修改装饰器,如果你选择把elementDecorators或者把buttonDecorators设置成静态,你甚至可以在表单初始化的时候注入默认的装饰器,允许在不同的装饰器堆栈中重用。
最后,你还可以通过重写loadDefaultDecorators()方法创建你自己的子类元素-根据你计划生成的不同类型的输出来使用不同的元素。
?
示例:使用ViewScript装饰器完全定制假如你想将内容里任意的混合进你的元素,或者你可能要使用一个复杂的标记?装饰器对这种个别的元素还是很好使的,但是对于一个整体的表单来说,他就没有多大意义了,还是用ViewScript 装饰器吧。
ViewScript装饰器将把给定的视图脚本作为一部分呈现,传递表单作为$form视图变量,它可以允许你取出你需要元数据和(或者)元素来直接的呈现他们。当把它用在表单对象的时候,它通常会排除显示组的需要,你可以手动来做这些。
上个例子,考虑一下下面的视图脚本:
?
?
<h4>Please register with us!</h4> <form action="<?= $this->escape($this->form->getAction()) ?>" method="<?= $this->escape($this->form->getMethod()) ?>"> <fieldset> <legend>Demographics</legend> <p> Please provide us the following information so we can know more about you. </p> <?= $this->form->age ?> <?= $this->form->nationality ?> <?= $this->form->income ?> </fieldset> <fieldset> <legend>User Information</legend> <p> Now please tell us who you are and how to contact you. </p> <?= $this->form->firstname ?> <?= $this->form->lastname ?> <?= $this->form->email ?> <?= $this->form->address1 ?> <?= $this->form->address2 ?> <?= $this->form->city ?> <?= $this->form->state ?> <?= $this->form->postal ?> <?= $this->form->phone ?> </fieldset> <?= $this->form->submit ?> </form>?
?
?
假如上面的表单在视图脚本 “demogForm.phtml”,你可以把下面这段代码附加到你的表单
?
?
$form->setDecorators(array( array('ViewScript', array('script' => 'demogForm.phtml')) ));?
?
你可以看到,这个呈现表单的方法更加详细,它几乎允许你无限制的调整表单,增加由装饰器提供的高级的错误报告,标签,等等(通过使用与元素相关的装饰器)
?
创建一个自定义装饰器完全会有这种时候,标准的装饰器不能够胜任我们的需要,你可能会感觉通过一串的装饰器来生成它太复杂了,你可能会想消减你的方法来优化程序,你可能会合并多个分散的HTML元素成一个Zend_Form元素,等等。这时你应该研究一下自定义装饰器了。
装饰器的解刨学Zend_Form所有的装饰器实现了Zend_Form_Decorator_Interface 接口,就像这个样子:
?
?
interface Zend_Form_Decorator_Interface { public function __construct($options = null); public function setElement($element); public function getElement(); public function setOptions(array $options); public function setConfig(Zend_Config $config); public function setOption($key, $value); public function getOption($key); public function getOptions(); public function removeOption($key); public function clearOptions(); public function render($content); }?
?
为了你的方便,大部分方法在Zend_Form_Decorator_Abstract 中已经实现,意味着你需要做的只是为装饰器提供一个render()方法:
?
?
class My_Form_Decorator_Foo extends Zend_Form_Decorator_Abstract { public function render($content) { // ... return $content } }?
?
一个装饰器存储了一个当前对象的实例作为一个‘元素’。然而这个元素受限于Zend_Form_Element对象,但是也可以是表单,显示组,或者子表单。
这样一来,你可以对这些对象类型和元数据的任意方面指定装饰器,通过使用getElement()检索到当前的‘元素’。
?
假如你的装饰器应该被视图识别,你可以尝试把视图对象从元素中提取出来
?
?
$view = $this->getElement()->getView();?
?
假如返回的值是空值,说明你没有这是视图。
?
separator和placement这两个属性应该是必须设置的,render方法在返回内容时要用到他们。
当继承Zend_Form_Decorator_Abstract 时,你可以通过getSeparator()和getPlacement()来检索到他们,或者通过使用getOption()来获得,就像他们作为参数被传递进去一样,例如:
?
?
public function render($content) { // ... $separator = $this->getSeparator(); $placement = $this->getPlacement(); switch ($placment) { case 'APPEND': // append original content with new content return $content . $separator . $newContent; case 'PREPEND': // prepend original content with new content return $newContent . $separator . $content; default: // replace otherwise return $newContent; } }?
?
??
既然现在已经有办法创建装饰器了,那接下来让我们看看如何告诉表单去找到他们
?
?
把装饰器告诉你的表单或者元素创建自定义装饰器很列害,但假如你不能把装饰器告诉表单也是没用的,你应该告诉你的对象去哪里寻找装饰器
表单你可以使用addPrefixPath() 来告诉表单去哪里查找装饰器:
?
?
// Basic usage: $form->addPrefixPath($prefix, $path, 'decorator'); // Example usage: $form->addPrefixPath('My_Form_Decorator', 'My/Form/Decorator/', 'decorator');?
?
第二个参数$path指名了包含前缀的类的路径,假如那个路径在你的include_path中,你可以使用一个相对路径,假如不在使用一个完全路径,以确保插件载入器可以找到这个类。
?
第三个参数是应用路径的插件的类型;在这里我们只是研究的装饰器,所以我们使用的字符串是'decorator';然而,你也可以使用addPrefixPath()来设置其他类型插件的路径。例如elements,validators,和filters。
?
你也可以为多种Zend_Form聚合元素来设置装饰器路径,这可以通过使用下面的方法做到:
?
?
addElementPrefixPath($path, $prefix, 'decorator')addDisplayGroupPrefixPath($path, $prefix)?
?
?
在每种情况下,这个设置将及时应用于被附加到表单的元素或者显示组,也会应用到那些后来被创建或添加的元素,你也可以在配置的时候传递一个elementPrefixPath参数,这个值将会被表单里所有元素使用。例如:
?
?
// Programmatically $form->addElementPrefixPath('My_Form_Decorator', 'My/Form/Decorator', 'decorator'); $form->addDisplayGroupPrefixPath('My_Form_Decorator', 'My/Form/Decorator'); // at instantiation: $form = new Zend_Form(array( 'elementPrefixPath' => array( array( 'prefix' => 'My_Form_Decorator', 'path' => 'My/Form/Decorator/', 'type' => 'decorator' ), ), )); // Or using an INI config file: form.elementPrefixPath.my.prefix = "My_Form_Decorator" form.elementPrefixPath.my.path = "My/Form/Decorator" form.elementPrefixPath.my.type = "decorator" // Or using an XML config file: <form> <elementPrefixPath> <my> <prefix>My_Form_Decorator</prefix> <path>My/Form/Decorator</path> <type>decorator</type> </my> </elementPrefixPath> </form>元素
为一个单独的元素设置一个插件路径,你可以调用元素本身的addPrefixPath($prefix, $path, 'decorator'),或者通过prefixPath作为键传递一个值。
假如你知道只有单一的或者元素子集需要使用这个装饰器插件,那么这个方法应该是最有用的了,或者你需要一些元素使用标准的装饰器其他的使用重写的装饰器。
?
这个用例几乎和Zend_Form 完全一样
?
?
// 基本用法: $element->addPrefixPath($prefix, $path, 'decorator'); // 示例: $element->addPrefixPath('My_Form_Decorator', 'My/Form/Decorator', 'decorator'); // 实例配置: $element = new Zend_Form_Element('foo', array( 'prefixPath' => array( array( 'type' => 'decorator', 'path' => 'My/Form/Decorator/', 'prefix' => 'My_Form_Decorator' ), ), )); // 或者伴随着表单对象创建: $form->addElement( 'text', 'foo', array( 'prefixPath' => array( array( 'type' => 'decorator', 'path' => 'My/Form/Decorator/', 'prefix' => 'My_Form_Decorator' ), ), )); // 通过ini配置: form.foo.options.prefixPath.my.type = "decorator" form.foo.options.prefixPath.my.path = "My/Form/Decorator/" form.foo.options.prefixPath.my.prefix = "My_Form_Decorator" //通过XML配置: <form> <element> <options><prefixPath> <my> <type>decorator</type> <path>My/Form/Decorator/</path> <prefix>My_Form_Decorator</prefix> </my> </prefixPath> </options> </element> </form>?
?
?
示例:分组的复选框既然你已经知道了装饰器如何工作,怎么写一个你自己的装饰器,然后如何告诉你的对象去哪找到它们,我们来试试吧。
?
在我们的案例中,我们要创建一组复选框指向标签。这些复选框将在一个两级结构的数组中,第一级是分类,第二级是复选框的值/标签对。我们要将每种分类标签归类到fieldset并且分类名作为legend,并把每个复选框和对应的标签列出。
?
另一件我们要做的事是确认每个复选框的标签都被翻译过。
?
我们描述性的把装饰器装饰器叫做 'CategorizedCheckbox’,因为我们是开发者,我们要使用自己命名的类,它要以 'My_Form_Decorator’为前缀。
?
开始前,我们首先要注册一个元素,并且这个元素继承于Zend_Form_Element_Multi;我们知道getValue()将返回一个数组,此外,我们可以使用Zend_View视图助手,所以我们需要确认这个元素是否注册了视图对象。最后,我们将获得翻译对象留着后面使用。
?
?
class My_Form_Decorator_CategorizedCheckbox extends Zend_Form_Decorator_Abstract { public function render($content) { $element = $this->getElement(); if (!$element instanceof Zend_Form_Element_Multi) { return $content; } if (null === ($view = $element->getView())) { return $content; } $translator = $element->getTranslator(); } }?
?
?
既然我们已经开始,让我们来做些工作吧。首先我们要初始化我们想创建的新的内容,接着可以抓取到当前值,我们可以测试一下这个复选框稍候是不是要选中。然后我们需要得到元素的名字以至于我们可以把它当作一个单独的复选框,然后创建复选框的ID,最后,我们可以迭代出所有参数:
?
class My_Form_Decorator_CategorizedCheckbox extends Zend_Form_Decorator_Abstract { public function render($content) { // ... $html = ''; $values = (array) $element->getValue(); $baseName = $element->getName(); foreach ($element->getMultiOptions() as $category => $values) { } } }?
?
现在好玩的要到来了:生成标签。因为fieldsets包含着内容,我们将先生成复选框列表,然后把它们包裹在fieldset中。我们可以通过视图助手做到这些,这里也是我们翻译标签的位置,复选框需要名字和id,我们也一同在这里创建,把他们传入formCheckbox()视图助手,当一个类别的所有的选项都完成后,我们将使用fieldset包裹它们,使用翻译的类别作为legend。
?
?
class My_Form_Decorator_CategorizedCheckbox extends Zend_Form_Decorator_Abstract{ public function render($content) { // ... foreach ($element->getMultiOptions() as $category => $values) { $boxes = '<ul name="code">class My_Form_Decorator_CategorizedCheckbox extends Zend_Form_Decorator_Abstract{ public function render($content) { // ... $placement = $this->getPlacement(); $separator = $this->getSeparator(); switch ($placement) { case 'APPEND': return $content . $separator . $html; case 'PREPEND': return $html . $separator . $content; case null: default: return $html; } }}?
?
??
这样就搞定了,我们现在可以把它设置成一个元素的装饰器了,完活。
其他定制装饰器的方法大部分装饰器有多个参数,你可以到http://framework.zend.com/manual/en/zend.form.standardDecorators.html去查看文档来决定使用什么参数。很多参数可以使你定制输出内容,所以提供值或者修改他们是个简单的定制表单的办法。
有很多方法是可以设置参数的:
另一个修改你的装饰器并且定制视图助手装饰器的方法是覆写你的视图助手,使用你自己的视图助手覆盖原有的视图助手在前面的教程中已经提到,这也是定制表单的一个可行办法。
总结装饰器是提供复杂方法的简单的类:为你的元素生成复杂的标签,希望有了这边教程,你可以结合装饰器并且写出你自定义的装饰器来实现适合你的网站的标签。
?
原文地址:http://devzone.zend.com/1240/decorators-with-zend_form/
?
?