在我上一篇关于装饰器的文章中,我有一个显示“出生日期”元素的示例:
<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装饰器
