我一直看到关于Zend_Form装饰器的咆哮和普遍的困惑(以及偶尔的赞美),我想我会做一个迷你系列的博客文章来展示它们是如何工作的。
首先,装饰器设计模式的一些背景知识。一种常用技术是定义一个通用接口,您的原始对象和装饰器都将实现该接口;你的装饰器接受原始对象作为依赖项,并将代理它或覆盖它的方法。让我们将其放入代码中以使其更易于理解:
interface Window { public function isOpen(); public function open(); public function close(); } class StandardWindow implements Window { protected $_open = false; public function isOpen() { return $this->_open; } public function open() { if (!$this->_open) { $this->_open = true; } } public function close() { if ($this->_open) { $this->_open = false; } } } class LockedWindow implements Window { protected $_window; public function __construct(Window $window) { $this->_window = $window; $this->_window->close(); } public function isOpen() { return false; } public function open() { throw new Exception('Cannot open locked windows'); } public function close() { $this->_window->close(); } }
然后您创建一个StandardWindow
类型的对象,将其传递给LockedWindow
的构造函数,您的窗口实例现在具有不同的行为。美妙之处在于您不必在标准窗口类上实现任何类型的“锁定”功能-装饰器会为您处理。与此同时,您可以将锁定的窗口当作另一个窗口一样传递。
装饰器模式的一个特别有用的地方是创建对象的文本表示。例如,您可能有一个“Person”对象,它本身没有文本表示。通过使用Decorator模式,您可以创建一个对象,它的行为就像一个人,但也提供了以文本方式呈现该人的能力。
在这个特定的示例中,我们将使用鸭子类型而不是显式接口。这使我们的实现更加灵活,同时仍然允许装饰器对象完全像Person对象一样工作。
class Person { public function setFirstName($name) {} public function getFirstName() {} public function setLastName($name) {} public function getLastName() {} public function setTitle($title) {} public function getTitle() {} } class TextPerson { protected $_person; public function __construct(Person $person) { $this->_person = $person; } public function __call($method, $args) { if (!method_exists($this->_person, $method)) { throw new Exception('Invalid method called on TextPerson: ' . $method); } return call_user_func_array(array($this->_person, $method), $args); } public function __toString() { return $this->_person->getTitle() . ' ' . $this->_person->getFirstName() . ' ' . $this->_person->getLastName(); } }
在此示例中,您将Person实例传递给TextPerson构造函数。通过使用方法重载,您可以继续调用Person的所有方法——设置名字、姓氏或头衔——但您现在还可以通过__toString()
获得字符串表示形式方法。
后一个例子接近于Zend_Form
装饰器的工作方式。关键区别在于,元素没有装饰器包装元素,而是附加了一个或多个装饰器,然后将其自身注入其中以进行渲染。然后,装饰器可以访问元素的方法和属性,以创建元素的表示或元素的子集。
Zend_Form
装饰器都实现了一个公共接口,Zend_Form_Decorator_Interface
。该接口提供了设置特定于装饰器的选项、注册和检索元素以及呈现的能力。基本装饰器Zend_Form_Decorator_Abstract
提供了您需要的大部分功能,渲染逻辑除外。
让我们考虑一种情况,我们只想将元素呈现为带有标签的标准表单文本输入。我们暂时不用担心错误处理或元素是否应该包含在其他标签中——只是一些基础知识。这样的装饰器可能看起来像这样:
class My_Decorator_SimpleInput extends Zend_Form_Decorator_Abstract { protected $_format = '<label for="%s">%s</label><input id="%s" name="%s" type="text" value="%s"/>'; public function render($content) { $element = $this->getElement(); $name = htmlentities($element->getFullyQualifiedName()); $label = htmlentities($element->getLabel()); $id = htmlentities($element->getId()); $value = htmlentities($element->getValue()); $markup = sprintf($this->_format, $id, $label, $id, $name, $value); return $markup; } }
让我们创建一个使用这个装饰器的元素:
$decorator = new My_Decorator_SimpleInput(); $element = new Zend_Form_Element('foo', array( 'label' => 'Foo', 'belongsTo' => 'bar', 'value' => 'test', 'decorators' => array($decorator), ));
呈现此元素会产生以下标记:
<label for="bar-foo">Foo</label><input id="bar-foo" name="bar[foo]" type="text" value="test"/>
你也可以把这个类放在你的库中的某个地方,通知你的元素那个路径,并将装饰器也简单地称为“SimpleInput”:
$element = new Zend_Form_Element('foo', array( 'label' => 'Foo', 'belongsTo' => 'bar', 'value' => 'test', 'prefixPath' => array('decorator' => array( 'My_Decorator' => 'path/to/decorators/', )), 'decorators' => array('SimpleInput'), ));
这为您提供了在其他项目中重用的好处,也为以后提供该装饰器的替代实现打开了大门(另一篇文章的主题)。
希望上面对装饰器模式的概述和这个简单的例子能对您如何开始编写装饰器有所启发。我将在接下来的几周内撰写更多帖子,展示如何利用装饰器构建更复杂的标记,并将更新这篇帖子以在编写时链接到它们。
更新:修正抛出的异常中的文本以反映实际的类名;根据David的评论,更新标签生成以将id用于“for”属性。
本系列的其他内容:
- 由内而外:如何对装饰器进行分层