在我上一篇关于装饰器的文章中,我有一个显示“出生日期”元素的示例:
<div class=\"element\"> <?php echo $form->dateOfBirth->renderLabel() ?> <?php echo $this->formText('dateOfBirth[day]', '', array( 'size' => 2, 'maxlength' => 2)) ?> / <?php echo $this->formText('dateOfBirth[month]', '', array( 'size' => 2, 'maxlength' => 2)) ?> / <?php echo $this->formText('dateOfBirth[year]', '', array( 'size' => 4, 'maxlength' => 4)) ?> </div>
这引发了一些关于如何将此元素表示为Zend_Form_Element
以及如何编写装饰器来封装此逻辑的问题。幸运的是,我已经计划好在这篇文章中解决这些问题!
元素
关于元素如何工作的问题包括:
- 您将如何设置和检索该值?
- 您将如何验证该值?
- 无论如何,您将如何允许三段式的离散形式输入(日、月、年)?
前两个问题围绕表单元素本身:setValue()
和getValue()
是如何工作的?关于装饰器的问题实际上还隐含了另一个问题:如何从元素中检索离散的日期段和/或设置它们?
解决方案是覆盖元素的setValue()
方法以提供一些自定义逻辑。在这种特殊情况下,我们的元素应该具有三个独立的行为:
- 如果提供了一个整数时间戳,它应该被用来确定和存储日、月、年
- 如果提供了一个文本字符串,它应该被转换为一个时间戳,并且然后用于确定和存储日、月和年的值
- 如果提供了包含日期、月和年的键的数组,则应存储这些值
在内部,日、月、年将被离散存储。当元素的值被检索时,它将以规范化的字符串格式完成。我们将覆盖getValue()
并将离散的日期段组装成最终字符串。
类应该是这样的:
<?php class My_Form_Element_Date extends Zend_Form_Element_Xhtml { protected $_dateFormat = '%year%-%month%-%day%'; protected $_day; protected $_month; protected $_year; public function setDay($value) { $this->_day = (int) $value; return $this; } public function getDay() { return $this->_day; } public function setMonth($value) { $this->_month = (int) $value; return $this; } public function getMonth() { return $this->_month; } public function setYear($value) { $this->_year = (int) $value; return $this; } public function getYear() { return $this->_year; } public function setValue($value) { if (is_int($value)) { $this->setDay(date('d', $value)) ->setMonth(date('m', $value)) ->setYear(date('Y', $value)); } elseif (is_string($value)) { $date = strtotime($value); $this->setDay(date('d', $date)) ->setMonth(date('m', $date)) ->setYear(date('Y', $date)); } elseif (is_array($value) && (isset($value['day']) && isset($value['month']) && isset($value['year']) ) ) { $this->setDay($value['day']) ->setMonth($value['month']) ->setYear($value['year']); } else { throw new Exception('Invalid date value provided'); } return $this; } public function getValue() { return str_replace( array('%year%', '%month%', '%day%'), array($this->getYear(), $this->getMonth(), $this->getDay()), $this->_dateFormat ); } }
这个类提供了一些很好的灵活性——我们可以从我们的数据库中设置默认值,并确保该值将被正确存储和表示。此外,我们可以允许从通过表单输入传递的数组中设置值。最后,我们为每个日期段提供了独立的访问器,我们现在可以在装饰器中使用它们来创建复合元素。
装饰器
回顾上一篇文章中的示例,假设我们希望用户分别输入年月日。幸运的是,PHP允许我们在创建元素时使用数组表示法,因此仍然可以将这三个实体捕获到一个值中—我们现在已经创建了一个可以处理这样一个数组值的Zend_Form
元素。
装饰器相对简单:它将从元素中获取日、月和年,并将每个传递给一个离散的视图助手以呈现单个表单输入;然后这些将被聚合以形成最终标记。
class My_Form_Decorator_Date extends Zend_Form_Decorator_Abstract { public function render($content) { $element = $this->getElement(); if (!$element instanceof My_Form_Element_Date) { // only want to render Date elements return $content; } $view = $element->getView(); if (!$view instanceof Zend_View_Interface) { // using view helpers, so do nothing if no view present return $content; } $day = $element->getDay(); $month = $element->getMonth(); $year = $element->getYear(); $name = $element->getFullyQualifiedName(); $params = array( 'size' => 2, 'maxlength' => 2, ); $yearParams = array( 'size' => 4, 'maxlength' => 4, ); $markup = $view->formText($name . '[day]', $day, $params) . ' / ' . $view->formText($name . '[month]', $month, $params) . ' / ' . $view->formText($name . '[year]', $year, $yearParams); switch ($this->getPlacement()) { case self::PREPEND: return $markup . $this->getSeparator() . $content; case self::APPEND: default: return $content . $this->getSeparator() . $markup; } } }
我们现在必须对我们的表单元素做一个小的调整,并告诉它我们想使用上面的装饰器作为默认值。这需要两个步骤。首先,我们需要通知装饰器路径的元素。我们可以在构造函数中这样做:
class My_Form_Element_Date extends Zend_Form_Element_Xhtml { // ... public function __construct($spec, $options = null) { $this->addPrefixPath( 'My_Form_Decorator', 'My/Form/Decorator', 'decorator' ); parent::__construct($spec, $options); } // ... }
请注意,我是在构造函数中而不是在init()
中执行此操作。这是有两个原因的。首先,它允许我稍后扩展元素以在init
中添加逻辑,而无需担心调用parent::init()
。其次,它允许我通过配置或在init
方法中传递额外的插件路径,然后允许我用我自己的替换覆盖默认的Date装饰器。
接下来,我们需要覆盖loadDefaultDecorators()
方法来使用我们的newDate装饰器:
class My_Form_Element_Date extends Zend_Form_Element_Xhtml { // ... public function loadDefaultDecorators() { if ($this->loadDefaultDecoratorsIsDisabled()) { return; } $decorators = $this->getDecorators(); if (empty($decorators)) { $this->addDecorator('Date') ->addDecorator('Errors') ->addDecorator('Description', array( 'tag' => 'p', 'class' => 'description') ) ->addDecorator('HtmlTag', array( 'tag' => 'dd', 'id' => $this->getName() . '-element') ) ->addDecorator('Label', array('tag' => 'dt')); } } // ... }
最终输出是什么样的?让我们考虑以下元素:
$d = new My_Form_Element_Date('dateOfBirth'); $d->setLabel('Date of Birth: ') ->setView(new Zend_View()); // These are equivalent: $d->setValue('20 April 2009'); $d->setValue(array('year' => '2009', 'month' => '04', 'day' => '20'));
如果您随后回显此元素,您将获得以下标记(为了便于阅读,对空白进行了一些轻微修改):
<dt id="dateOfBirth-label"><label for="dateOfBirth" class="optional"> Date of Birth: </label></dt> <dd id="dateOfBirth-element"> <input type="text" name="dateOfBirth[day]" id="dateOfBirth-day" value="20" size="2" maxlength="2"> / <input type="text" name="dateOfBirth[month]" id="dateOfBirth-month" value="4" size="2" maxlength="2"> / <input type="text" name="dateOfBirth[year]" id="dateOfBirth-year" value="2009" size="4" maxlength="4"> </dd>
结论
我们现在有一个元素可以呈现多个相关的表单输入字段,然后将聚合字段作为单个实体处理—dateOfBirth
元素将作为数组传递给该元素,并且该元素然后,正如我们之前提到的,将创建适当的日期段并返回一个我们可以用于大多数后端的值。
此外,我们可以对元素使用不同的装饰器。如果我们想使用DojoDateTextBoxdijit装饰器——它接受并返回字符串值——我们可以,无需修改元素本身。
最后,您将获得一个统一的元素API,您可以使用它来描述表示复合值的元素。
本系列其他文章:
- 最简单的Zend_Form装饰器
- 由内而外:如何对装饰器进行分层
- 单独呈现Zend_Form装饰器