本文来自正在翻译中的《
symfony权威
指南》第10章,译者James Hang
Chapter 10 - Forms 第十章 - 表单
==================
可以说, 表单占据了开发人员编写
模板的大部分时间, 而且表单一般都设计得相当糟糕. 由于涉及默认值, 数据格式, 验证, 重填, 表单处理等许
多内容, 开发者常常忽略了表单中的一些重要细节. 而symfony恰恰对这个问题给予了特别的关注. 本章介绍了为加速表单开发而设计的可以自动完
成多种要求的开发工具:
* 表单辅助函数提供了一种比较快地在模板中编写表单输入的方法, 特别是在编写诸如日期,下拉列表和富文本之类复杂的元素时.
* 如果要用一个表单去编辑一个对象的属性时, 利用对象表单辅助函数可以进一步加速模板的编写.
* YAML验证
文件可以方便表单验证和重填.
* 验证器集成了用于验证输入数据的代码, symfony绑定了满足最常用的需求的验证器, 开发人员也很容易定制自己的验证器.
Form Helpers 表单辅助函数
-----------------------
在模板中, 表单元素的
HTML标记常常和
PHP代码混杂在一起. symfony中的form 辅助函数就是为了减少这种情形的发生并且避免在
`<input>`标记中不断重复`<?php echo`标记.
### Main Form Tag主要的表单标记
根据前面章节的介绍, 你必须用`form_tag()` 辅助函数创建表单, 因为它可以将用参数表示的动作转换为经路由过的URL. 第二个参数还
可以支持额外的选项---例如,可以改变缺省的`method`, 可以改变缺省的`enctype`或指定其他的属性, 参见 例10-1.
例10-1 `form_tag()` 辅助函数
[php]
<?php echo form_tag('test/save') ?>
=> <form method="post" action="/path/to/save">
<?php echo form_tag('test/save', 'method=get multipart=true
class=simpleForm') ?>
=> <form method="get" enctype="multipart/form-data"
class="simpleForm"action="/path/to/save">
因为没有必要提供表单结束辅助函数, 所以尽管看起来不怎么美观, 你仍旧需要加上HTML的</form>标记.
### Standard Form Elements标准的表单元素
有了表单辅助函数, 表单中的每个元素都会默认以元素名作为其id属性. 这个约定很有用. 例10-2给出了所有标准表单 辅助函数及相关的选
项.
例10-2 标准表单辅助函数语法.
[php]
// Text field (input)文本域
<?php echo input_tag('name', 'default value') ?>
=> <input type="text" name="name" id="name" value="default
value" />
// All form helpers accept an additional options parameter所有表单辅助函数都
接受一个额外的选项参数.
// It allows you to add custom attributes to
the generated tag它允许你为
生成的标记加上定制的属性.
<?php echo input_tag('name', 'default value', 'maxlength=20') ?>
=> <input type="text" name="name" id="name" value="default
value"maxlength="20" />
// Long text field (text area)长文本域
<?php echo textarea_tag('name', 'default content', 'size=10x20') ?
=> <textarea name="name" id="name" cols="10" rows="20">
default content
</textarea>
// Check box复选框
<?php echo checkbox_tag('single', 1, true) ?>
<?php echo checkbox_tag('driverslicense', 'B', false) ?>
=> <input type="checkbox" name="single" id="single"
value="1"checked="checked" />
<input type="checkbox" name="driverslicense"
id="driverslicense"value="B" />
// Radio button单选按钮
<?php echo radiobutton_tag('status[]', 'value1', true) ?>
<?php echo radiobutton_tag('status[]', 'value2', false) ?>
=> <input type="radio" name="status[]" id="status_value1"
value="value1"checked="checked" />
<input type="radio" name="status[]" id="status_value2"
value="value2" />
// Dropdown list (select)下拉列表
<?php echo select_tag('payment',
'<option selected="selected">Visa</option>
<option>Eurocard</option>
<option>Mastercard</option>')
?>
=> <select name="payment" id="payment">
<option selected="selected">Visa</option>
<option>Eurocard</option>
<option>Mastercard</option>
</select>
// List of options
for a select tag可选项列表
<?php echo options_for_select(array('Visa', 'Eurocard',
'Mastercard'), 0) ?>
=> <option value="0" selected="selected">Visa</option>
<option value="1">Eurocard</option>
<option value="2">Mastercard</option>
// Dropdown helper combined with a list of options混合了可选项的下拉列表辅助函数
<?php echo select_tag('payment', options_for_select(array(
'Visa',
'Eurocard',
'Mastercard'
), 0)) ?>
=> <select name="payment" id="payment">
<option value="0" selected="selected">Visa</option>
<option value="1">Eurocard</option>
<option value="2">Mastercard</option>
</select>
// To specify option names, use an associative array 用关联数组指明选项名称
<?php echo select_tag('name', options_for_select(array(
'Steve' => 'Steve',
'Bob' => 'Bob',
'Albert' => 'Albert',
'Ian' => 'Ian',
'Buck' => 'Buck'
), 'Ian')) ?>
=> <select name="name" id="name">
<option value="Steve">Steve</option>
<option value="Bob">Bob</option>
<option value="Albert">Albert</option>
<option value="Ian" selected="selected">Ian</option>
<option value="Buck">Buck</option>
</select>
// Dropdown list with multiple selection (selected values can be
an array)
<?php echo select_tag('payment', options_for_select(
array('Visa' => 'Visa', 'Eurocard' => 'Eurocard', 'Mastercard'
=> 'Mastercard'),
array('Visa', 'Mastecard'),
), array('multiple' => true))) ?>
=> <select name="payment[]" id="payment" multiple="multiple">
<option value="Visa" selected="selected">Visa</option>
<option value="Eurocard">Eurocard</option>
<option value="Mastercard">Mastercard</option>
</select>
// Drop-down list with multiple selection (selected values can be
an array)可复选的下拉列表(可选值可以是一个数组)
<?php echo select_tag('payment', options_for_select(
array('Visa' => 'Visa', 'Eurocard' => 'Eurocard', 'Mastercard'
=> 'Mastercard'),
array('Visa', 'Mastecard')
), 'multiple=multiple') ?>
=> <select name="payment" id="payment" multiple="multiple">
<option value="Visa" selected="selected">
<option value="Eurocard">Eurocard</option>
<option value="Mastercard" selected="selected">Mastercard</
option>
</select>
// Upload file field上传文件域
<?php echo input_file_tag('name') ?>
=> <input type="file" name="name" id="name" value="" />
// Password field密码域
<?php echo input_password_tag('name', 'value') ?>
=> <input type="password" name="name" id="name" value="value" />
// Hidden field隐藏域
<?php echo input_hidden_tag('name', 'value') ?>
=> <input type="hidden" name="name" id="name" value="value" />
// Submit button (as text)提交按钮(文本格式)
<?php echo submit_tag('Save') ?>
=> <input type="submit" name="submit" value="Save" />
// Submit button (as image)提交按钮(图片格式)
<?php echo submit_image_tag('submit_img') ?>
=> <input type="image" name="submit" src="/images/
submit_img.png" />
submit_image_tag() 辅助函数使用的语法和image_tag()相同, 也具有相同的优点.
>**NOTE**
>For radio buttons, the `id` attribute is not set by default to the value of the `name` attribute, but to a combination of the name and the value. That's because you need to have several radio button tags with the same name to obtain the automated "deselecting the previous one when selecting another" feature, and the `id=name` convention would imply having several HTML tags with the same `id` attribute in your page, which is strictly forbidden.注意: 对于单选按钮, id属性没有默认地被设定为name属性的值, 而是以name属性值和选项值的混合值作为默认值.因为你需要有多个同名的单选按钮标记,以便达到"选中一个就自动去除另一个"的目的, 这样一来,id和name取同样值的约定将会导致在
页面中有多个同样id属性的HTML标记, 而这是被严格禁止的
-
>**SIDEBAR**
>Handling form submission处理表单提交
>How do you retrieve the data submitted by users through forms? It is available in the request parameters, so the action only needs to call `$this->getRequestParameter($elementName)` to get the value.
>如何取得用户通过表单提交的数据呢? 数据在请求参数中, 所以动作只要调用$this->getRequestParameter($elementName)就可以取得数据.
>A good practice is to use the same action to display and handle the form. According to the request method (GET or POST), either the form
template is called or the form is handled and the request is redirected to another action.在同一个动作中显示和处理表单是一种比较好的方法.根据被调用的表单模板或被处理的表单的请求方法(GET或POST),请求会被重定向到另一个动作 .
> [php]
> // In mymodule/actions/actions.class.php
> public function executeEditAuthor()
> {
> if ($this->getRequest()->getMethod() != sfRequest::POST)
> {
> // Display the form
> return sfView::SUCCESS;
> }
> else
> {
> // Handle the form submission
> $name = $this->getRequestParameter('name');
> ...
> $this->redirect('mymodule/anotheraction');
> }
> }
>For this to work, the form target must be the same action as the one displaying it.
>要能正常工作, 表单处理必须和表单显示是同一个动作.
> [php]
> // In mymodule/templates/editAuthorSuccess.php
> <?php echo form_tag('mymodule/editAuthor') ?>
> ...
symfony还提供了为后台进行异步处理的专门表单 辅助函数.下一章有关
AJAX的介绍将会提供更详细的信息.
### Date Input Widgets数据输入控件.
表单常用于检索日期. 日期格式错误常常是表单提交失败的主要原因.如果你将rich选项设定为true, 则 input_date_tag()辅助
函数可以用一个交互式
JavaScript日历来帮助用户输入日期, 见图10-1所示.
图10-1 富日期输入标记

如果未设置rich选项, 则辅助函数将会输出三个select标记, 数据则包括月,日和年的范围.这三个标记有对应的辅助函数, 即
select_day_tag(), select_month_tag()和select_year_tag(), 你可以通过调用这三个辅助函数来
分别显示下拉列表.这些元素的缺省值是当前的年月日。例10-3演示了日期输入辅助函数的
应用。
例10-3 日期输入辅助函数
[php]
<?php echo input_date_tag('dateofbirth', '2005-05-03',
'rich=true') ?>
=> a text input tag together with a calendar widget
// The following helpers require the Date helper group
<?php use_helper('Date') ?>
<?php echo select_day_tag('day', 1, 'include_custom=Choose a
day') ?>
=> <select name="day" id="day">
<option value="">Choose a day</option>
<option value="1" selected="selected">01</option>
<option value="2">02</option>
...
<option value="31">31</option>
</select>
<?php echo select_month_tag('month', 1, 'include_custom=Choose a
month use_short_month=true') ?>
=> <select name="month" id="month">
<option value="">Choose a month</option>
<option value="1" selected="selected">Jan</option>
<option value="2">Feb</option>
...
<option value="12">Dec</option>
</select>
<?php echo select_year_tag('year', 2007, 'include_custom=Choose a
year year_end=2010') ?>
=> <select name="year" id="year">
<option value="">Choose a year</option>
<option value="2006">2006</option>
<option value="2007" selected="selected">2007</option>
...
</select>
input_date_tag() 辅助函数可接受的日期值是PHP函数strtotime()可以识别的值。 例10-4显示了哪些格式是可以使用
的, 而例10-5则指出哪些格式是要严格禁止使用的。
例10-4 日期辅助函数可以接受的日期格式
[php]
// Work fine
<?php echo input_date_tag('test', '2006-04-01', 'rich=true') ?>
<?php echo input_date_tag('test', 1143884373, 'rich=true') ?>
<?php echo input_date_tag('test', 'now', 'rich=true') ?>
<?php echo input_date_tag('test', '23 October 2005', 'rich=true') ?
<?php echo input_date_tag('test', 'next tuesday', 'rich=true') ?>
<?php echo input_date_tag('test', '1 week 2 days 4 hours 2
seconds', 'rich=true') ?>
// Return null
<?php echo input_date_tag('test', null, 'rich=true') ?>
<?php echo input_date_tag('test', '', 'rich=true') ?>
例10-5 日期辅助函数的错误日期格式
[php]
// Date zero = 01/01/1970
<?php echo input_date_tag('test', 0, 'rich=true') ?>
// Non-English date formats don't work
<?php echo input_date_tag('test', '01/04/2006', 'rich=true') ?>
### Rich Text Editing编辑富文本
因为集成了TinyMCE和FCKEditor插件, `<textarea>`标记的文本可以进行富文本编辑, 也即可以用类似字处理器的按钮来将文
本格式化为粗体,斜体或其它样式, 见图10-2.
图10-2 编辑复文本

这两种插件都需要手工安装。 因为安装方法相同, 这里只介绍TinyMCEDE 的安装方法。 你可以从该
项目的网站([http://
tinymce.moxiecode.com/](
http://tinymce.moxiecode.com/))
下载并解压到一个临时目录中, 然
后将`tinymce/jscripts/tiny_mce/`目录复制到你的项目的 `web/js/` 目录, 并在
`settings.yml`中设置指向这个库的路径,如例10-6所示.
例10-6 设置TinyMCE库路径
all:
.settings:
rich_text_js_dir: js/tiny_mce
设置好后, 再加入`rich=true`选项, 就可以再文本区内进行复文本编辑了。 你也可以用`tinymce_options`选项为
Javascript编辑器指定定制的选项。 参见例10-7
例 10-7 富文本区域
[php]
<?php echo textarea_tag('name', 'default content', 'rich=true
size=10x20')) ?>
=> a rich text edit zone powered by TinyMCE
<?php echo textarea_tag('name', 'default content', 'rich=true
size=10x20tinymce_options=language:"fr",theme_advanced_buttons2:"separator"')) ?
=> a rich text edit zone powered by TinyMCE with custom parameters
### Country and Language Selection选择国家和语言
你可能会需要一个可以选择国家的域。因为在不同的语言中国家名也不相同, 所以国家名下拉列表要根据用户的culture值来调整(第13章有
culture的详细介绍)。 select_country_tag() 辅助函数可以完成所有工作, 它会显示国家名, 并用ISO标准的国家代码
来作为选项的值,参见例10-8。
例10-8 国家列表辅助函数
[php]
<?php echo select_country_tag('country', 'AL') ?>
=> <select name="country" id="country">
<option value="AF">Afghanistan</option>
<option value="AL" selected="selected">Albania</option>
<option value="DZ">Algeria</option>
<option value="AS">American Samoa</option>
...
类似于select_country_tag() 辅助函数, select_language_tag() 辅助函数可以显示一个语言列表, 见例
10-9
例10-9 语言列表辅助函数
[php]
<?php echo select_language_tag('language', 'en') ?>
=> <select name="language" id="language">
...
<option value="elx">Elamite</option>
<option value="en" selected="selected">English</option>
<option value="enm">English, Middle (1100-1500)</option>
<option value="ang">English, Old (ca.450-1100)</option>
<option value="myv">Erzya</option>
<option value="eo">Esperanto</option>
...
Form Helpers for Objects对象表单辅助函数
---------------------------------------
如果用表单的元素去编辑对象的属性, 那么用标准的链接辅助函数去写代码非常费时. 比如, 要编辑Customer对象的telephone属性,
应该写成:
[php]
<?php echo input_tag('telephone', $customer->getTelephone()) ?>
=> <input type="text" name="telephone" id="telephone"
value="0123456789" />
为了避免重复地写属性名, symfony为每个表单辅助函数另外提供了一个对象表单辅助函数. 对象表单辅助函数将从对象和方法名来获得表单元素的名
字和缺省值. 前面用过的input_tag()可以变换为如下形式:
[php]
<?php echo object_input_tag($customer, 'getTelephone') ?>
=> <input type="text" name="telephone" id="telephone"
value="0123456789" />
虽然 object_input_tag()看上去并不省事, 但是, 每个标准的表单辅助函数都有一个对应的对象表单辅助函数, 并且它们的语法都一
样, 这样要生成表单就非常简单. 这也是为什么在symfony生成的
框架和后台管理都广泛使用了对象表单辅助函数(详见第14章). 例10-10
展示了对象表单辅助函数的用法.
例10-10 对象表单辅助函数语法
[php]
<?php echo object_input_tag($object, $method, $options) ?>
<?php echo object_input_date_tag($object, $method, $options) ?>
<?php echo object_input_hidden_tag($object, $method, $options) ?>
<?php echo object_textarea_tag($object, $method, $options) ?>
<?php echo object_checkbox_tag($object, $method, $options) ?>
<?php echo object_select_tag($object, $method, $options) ?>
<?php echo object_select_country_tag($object, $method, $options) ?
<?php echo object_select_language_tag($object, $method, $options) ?
因为为一个密码元素预先设定缺省值不是一个好主意, 所以没有提供object_password_tag() 辅助函数.
>**CAUTION**
>Unlike the regular form helpers, the object form helpers are available only if you declare explicitly the use of the `Object` helper group in your template with `use_helper('Object')`.注意 与一般的表单辅助函数不同, 只有你在模板中用use_辅助函数('Object')生命使用'Object' 辅助函数组后才能使用对象表单辅助函数组.
对象表单辅助函数中最有趣的就是objects_for_select()和object_select_tag()两个, 它们都与下拉列表有关.
### Populating Drop-Down Lists with Objects生成对象的下拉列表.
例10-11中用options_for_select() 辅助函数和其他标准辅助函数, 将一个PHP关联数组转化为一个选项列表.
例10-11 用options_for_select()产生一个基于数组的选项列表.
[php]
<?php echo options_for_select(array(
'1' => 'Steve',
'2' => 'Bob',
'3' => 'Albert',
'4' => 'Ian',
'5' => 'Buck'
), 4) ?>
=> <option value="1">Steve</option>
<option value="2">Bob</option>
<option value="3">Albert</option>
<option value="4" selected="selected">Ian</option>
<option value="5">Buck</option>
假设你已经有了一个从Propel查询得到的类Author的对象数组, 如果你想建立一个基于这个数组的选项列表, 你需要用一个循环遍历每个对象的
id和name项, 如例10-12所示.
例10-12 用options_for_select()产生一个基于对象数组的选项列表
[php]
// In the action
$options = array();
foreach ($authors as $author)
{
$options[$author->getId()] = $author->getName();
}
$this->options = $options;
// In the template
<?php echo options_for_select($options, 4) ?>
因为经常需要进行这类处理, 所以symfony提供了一个objects_for_select() 辅助函数来直接从一个对象数组创建一个选项列
表。 这个辅助函数需要两个另外的参数:用于遍历值的方法名和生成<option>标记的文本内容。 例10-12可以简化为如下形式:
[php]
<?php echo objects_for_select($authors, 'getId', 'getName', 4) ?>
这样已经既快又好, 但是当你要处理外键列时, symfony还提供了更多的方便。
### Creating a Drop-Down List Based on a Foreign Key Column创建一个基于外键列的下拉
列表。
外键列可取的值是外键所在的表的记录的主键的键值。 例如, 如果article表有一个author_id列, 它是author表的的外键, 那么
这个列的可能值就是author表的所有id列的值。通常, 用于编辑一篇文章的作者的下拉列表看上去如例10-13所示。
例10-13 用objects_for_select()创建一个基于外键的选项列表
[php]
<?php echo select_tag('author_id', objects_for_select(
AuthorPeer::doSelect(new Criteria()),
'getId',
'__toString()',
$article->getAuthorId()
)) ?>
=> <select name="author_id" id="author_id">
<option value="1">Steve</option>
<option value="2">Bob</option>
<option value="3">Albert</option>
<option value="4" selected="selected">Ian</option>
<option value="5">Buck</option>
</select>
实际上,仅仅用 object_select_tag()就可以完成所有的工作。 它显示一个下拉列表, 其选项为外键所在表的所有可能记录值。 这个
辅助函数可以根据
数据库模式文件猜出外键列和外键所在的表, 因此它的语法非常简洁。以下代码和例10-13完成同样的功能:
[php]
<?php echo object_select_tag($article, 'getAuthorId') ?>
object_select_tag() 辅助函数根据作为参数传递的方法名就可以得到相关的peer类名(在本例中就是AuthorPeer). 但
是,你可以在第3个参数relate_class中设定你自己的类名。<option>标记的文本内容是是根据类的_toString()方法得到的记
录名(如果$author->_toString()方法没有
定义, 就用主键代替)。另外, 选项列表是从带有空条件的doSelect()方法运行
得出的结果中取得的, 它根据记录的创建时间排序。如果你想按指定的顺序显示记录的一个子集, 你可以在peer类中创建一个方法, 该方法返回满足条
件的对象数组, 然后你就可以设定peer_method选项。最后, 你可以通过设定include_blank或include_custom选
项,在下拉列表的顶部加一个空的选项或某个定制的选项。例10-14显示了object_select_tag() 辅助函数的各种选项的用法。
例10-14 object_select_tag() 辅助函数的选项
[php]
// Base syntax
<?php echo object_select_tag($article, 'getAuthorId') ?>
// Builds the list from AuthorPeer::doSelect(new Criteria())
// Change the peer class used to retrieve the possible values
<?php echo object_select_tag($article, 'getAuthorId',
'related_class=Foobar') ?>
// Builds the list from FoobarPeer::doSelect(new Criteria())
// Change the peer method used to retrieve the possible values
<?php echo object_select_tag($article,
'getAuthorId','peer_method=getMostFamousAuthors') ?>
// Builds the list from AuthorPeer::getMostFamousAuthors(new
Criteria())
// Add an <option value=""> </option> at the top of the list
<?php echo object_select_tag($article, 'getAuthorId',
'include_blank=true') ?>
// Add an <option value="">Choose an author</option> at the top of
the list
<?php echo object_select_tag($article, 'getAuthorId',
'include_custom=Choose an author') ?>
### Updating Objects更新对象
如果利用对象辅助函数编写专用于编辑对象属性的表单, 就比在动作中进行处理容易。 例如, 如果你有一个类Author包含name, age,
address等属性, 你就可以如10-15那样编写表单:
例10-15 仅用对象辅助函数的表单
[php]
<?php echo form_tag('author/update') ?>
<?php echo object_input_hidden_tag($author, 'getId') ?>
Name: <?php echo object_input_tag($author, 'getName') ?><br />
Age: <?php echo object_input_tag($author, 'getAge') ?><br />
Address: <br />
<?php echo object_textarea_tag($author, 'getAddress') ?>
</form>
提交表单时, 将调用author模块的update动作, 该动作只要调用由Propel生成的fromArray()方法就可以更新对象,参见例
10-16.
例10-16 基于对象表单辅助函数的表单提交处理函数。
[php]
public function executeUpdate ()
{
$author = AuthorPeer::retrieveByPk($this-
>getRequestParameter('id'));
$this->forward404Unless($author);
$author->fromArray($this->getRequest()->getParameterHolder()-
>getAll(),AuthorPeer::TYPE_FIELDNAME);
$author->save();
return $this->redirect('/author/show?id='.$author->getId());
}
Form Validation表单验证
-----------------------
第六章介绍过如何在动作类中用validateXXX()方法验证请求参数. 但是, 如果你用这种方法去验证表单提交的话, 你就会没完没了地写同样
的代码. symfony提供了一种
技术, 不用动作类中的PHP代码, 而仅仅是用一个YAML文件就可以验证表单.
为了说明表单验证的特性, 让我们先看一下例10-17中的表单示例. 这是一个典型的联系人表单, 包括name, email, age和
message域。
例 10-17 联系人表单示例. 文件路径为php `modules/contact/templates/indexSuccess.php`
[php]
<?php echo form_tag('contact/send') ?>
Name: <?php echo input_tag('name') ?><br />
Email: <?php echo input_tag('email') ?><br />
Age: <?php echo input_tag('age') ?><br />
Message: <?php echo textarea_tag('message') ?><br />
<?php echo submit_tag() ?>
</form>
表单验证的要求是: 如果用户想提交一个输入了无效数据的表单,
浏览器将显示包含出错信息的网页. 我们先对上面的表单定义什么样的数据才是有效
的.
*name域是必需的, 且必须是2到100个字符的文本.
*email域是必需的, 且必须是2到100个字符的文本, 并是一个有效的
电子邮件地址
*age域是必需的, 且必须是0-100之间的整数.
*message域是必需的.
你当然可以为联系人表单定义更复杂的验证规则, 不过在本例中有了上面定义的规则就足以说明问题了.
>**NOTE**
>Form validation can occur on the server side and/or on the client side. The server-side validation is compulsory to avoid corrupting a database with wrong data. The client-side validation is optional, though it greatly enhances the user experience. The client-side validation is to be done with custom JavaScript.
表单验证可以在
服务器端进行, 也可以在客户端进行.为了防止错误数据破坏数据库, 服务器端的验证是必需的. 尽管客户端的验证可以极大地提高用户体
验, 但客户端的验证却是可选的. 客户端验证通常用JavaScript定制.
### Validators验证器
你可以看到例子中的name和email域使用同样的验证规则.有些验证规则在
web表单中经常出现, 为此, symfony将这些规则的PHP代码
打包后构成验证器. 一个验证器是一个提供了execute()方法的简单的类. 这个方法以域的值为参数, 如果值有效则返回true, 否则返回
false.
symfony包含多个验证器(本章后面的"标准symfony验证器"一节将详细介绍), 这里我们先重点说明一下
sfStringValidator. 该验证器检测输入的是否为一个字符串, 切字符数在两个指定的值之间(通过调用initialize()方法定
义). 这正是我们验证name域时所需要的. 例10-18显示了如何在一个验证方法中使用这个验证器.
例10-18 用可复用的验证器验证请求参数, 文件路径为`modules/contact/action/actions.class.php`
[php]
public function validateSend()
{
$name = $this->getRequestParameter('name');
// The name field is required
if (!$name)
{
$this->getRequest()->setError('name', 'The name field cannot
be left blank');
return false;
}
// The name field must be a text entry between 2 and 100
characters
$myValidator = new sfStringValidator();
$myValidator->initialize($this->getContext(), array(
'min' => 2,
'min_error' => 'This name is too short (2 characters
minimum)',
'max' => 100,
'max_error' => 'This name is too long. (100 characters
maximum)',
));
if (!$myValidator->execute($name))
{
return false;
}
return true;
}
如果用户在例10-17中的表单的name域里输入值a, 则sfStringValidator的execute()方法将返回false (因为字
符串长度小于2), 因而validateSend()方法也返回false. 这样, executeSend()方法将不执行, 而是执行
handleErrorSend()方法.
>**TIP**
>The `setError()` method of the `sfRequest` method gives information to the template so that it can display an error message (as explained in the "Displaying the Error Messages in the Form" section later in this chapter). The validators set the errors internally, so you can define different errors for the different cases of nonvalidation. That's the purpose of the `min_error` and `max_error` initialization parameters of the `sfStringValidator`.sfRequest对象的setError()方法将要显示的出错信息提供给模板(本章后面的"显示表单的出错信息"一节将会详细说明). 验证器在内部设定了错误信息, 所以你可以为非验证的不同情形定义不同的错误信息.
本例中定义的所有规则都可以用验证器替代:
* `name`: `sfStringValidator` (`min=2`, `max=100`)
* `email`: `sfStringValidator` (`min=2`, `max=100`) and
`sfEmailValidator`
* `age`: `sfNumberValidator` (`min=0`, `max=120`)
不过"域是必需的"这条规则却不是由验证器处理的.
### Validation File验证文件
虽然你可以在validateSend()方法中用验证器轻易地实现你的联系人表单验证, 但这意味着你要经常重复大量的代码. symfony用另一
种方法来定义表单的验证规则, 这就是用YAML文件. 例10-19给出了name域的验证规则的转换, 其验证结果与例10-18得到的相同.
例10-19 验证文件, 路径为 `modules/contact/validate/send.yml`
fields:
name:
required:
msg: The name field cannot be left blank
sfStringValidator:
min: 2
min_error: This name is too short (2 characters minimum)
max: 100
max_error: This name is too long. (100 characters maximum)
在一个验证文件里, fields头列出了所有需要验证的域,并且如果有值时, 验证器必须对该值进行检验. 每个验证器的参数和你手工初始化使用的参
数相同. 只要需要, 一个域可以被多个验证器验证.
>**NOTE**
>The validation process doesn't stop when a validator fails. Symfony tests all the validators and declares the validation failed if at least one of them fails. And even if some of the rules of the validation file fail, symfony will still look for a `validateXXX()` method and execute it. So the two validation techniques are complementary. The advantage is that, in a form with multiple failures, all the error messages are shown.一个验证器验证失败并不会导致整个验证过程结束. symfony会检验所有的验证器, 只要其中一个失败, 则会报告整个验证过程失败. 即使验证文件的某些验证规则失败, symfony仍旧会找一个validateXXX()方法去执行. 所以两种验证技术是互补的. 好处就是对于有多个错误的表单, 可以揭示出所有的错误信息.
验证文件在模块的validate目录下, 并用需要验证的动作名称来命名. 例如, 例10-19的文件路径就是validate/
send.yml.
### Redisplaying the Form重新显示表单.
只要验证过程失败, symfony缺省地会在动作类中找一个handleErrorSend()方法, 如果没有这个方法, 就去显示
sendError.php模板.
不过, 告诉用户未能通过验证的更好方法却是再显示一遍包含出错信息的表单. 为此, 你需要重载handleErrorSend()方法, 并且重定
向到显示表单的动作去(上例中, 应该是module/index), 参见例10-20.
例10-20 再次显示表单, 文件路径为`modules/contact/actions/actions.class.php`
[php]
class ContactActions extends sfActions
{
public function executeIndex()
{
// Display the form
}
public function handleErrorSend()
{
$this->forward('contact', 'index');
}
public function executeSend()
{
// Handle the form submission
}
}
如果你用同一个动作显示表单和处理表单提交, 只要让handlErrorSend()方法简单地返回sfView::SUCCESS, 就可以从
sendSuccess.php重新显示表单,参见例10-21.
例10-21 一个动作同时显示和处理表单,文件路径为modules/contact/actions/actions.class.php
[php]
class ContactActions extends sfActions
{
public function executeSend()
{
if ($this->getRequest()->getMethod() != sfRequest::POST)
{
// Prepare data for the template
// Display the form
return sfView::SUCCESS;
}
else
{
// Handle the form submission
...
$this->redirect('mymodule/anotheraction');
}
}
public function handleErrorSend()
{
// Prepare data for the template
// Display the form
return sfView::SUCCESS;
}
}
有关数据准备的代码可以分离到动作类一个的保护方法中, 以免在executeSend()和handleErrorSend()方法中重复.
有了这个新的配置, 如果用户输入一个无效的名字, 表单就会重新显示, 但是输入的数据没有了,而且解释错误原因的出错信息也没有显示. 为解决这个
问题, 你必须修改显示表单的模板, 以便将出错信息显示在错误的域的旁边.
### Displaying the Error Messages in the Form在表单中显示出错信息.
当一个域验证失败时, 出错信息会作为一个验证器参数传送给请求(就象在例10-18中你用setError()手工添加错误一样).
sfRequest对象提供两个有用的方法用于查看错误信息: hasError()和getError(), 它们都以域名为参数. 另外, 你可以
借助hasErrors()在表单的顶部显示一个警告信息, 以引起用户注意有一个或多个域输入无效. 例10-22和10-23给出了如何使用这些方
法的例子.
例10-22 在表单顶部显示错误信息, 文件路径 `templates/indexSuccess.php`
[php]
<?php if ($sf_request->hasErrors()): ?>
<p>The data you entered seems to be incorrect.
Please correct the following errors and resubmit:</p>
<ul>
<?php foreach($sf_request->getErrors() as $name => $error): ?>
<li><?php echo $name ?>: <?php echo $error ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
例10-23 在表单内显示错误信息, 文件路径`templates/indexSuccess.php`
[php]
<?php echo form_tag('contact/send') ?>
<?php if ($sf_request->hasError('name')): ?>
<?php echo $sf_request->getError('name') ?> <br />
<?php endif; ?>
Name: <?php echo input_tag('name') ?><br />
...
<?php echo submit_tag() ?>
</form>
例10-23中包含getError()的条件句写起来有点长, 所以假如你预先声明了Validation辅助函数组,symfony提供了一个
form_error()辅助函数来替代这些长的代码.例10-24用这个辅助函数替换了例10-23的代码.
例10-24 用缩写方法在表单中显示出错信息
[php]
<?php use_helper('Validation') ?>
<?php echo form_tag('contact/send') ?>
<?php echo form_error('name') ?><br />
Name: <?php echo input_tag('name') ?><br />
...
<?php echo submit_tag() ?>
</form>
form_error()辅助函数在每个出错信息的前后分别加了一个特殊字符, 以使出错信息更醒目.这个字符的缺省值是向下的箭头(对应于
↓), 你可以在settings.yml文件里改变这个缺省值:
all:
.settings:
validation_error_prefix: ' ↓ '
validation_error_suffix: ' ↓'
现在,当验证失败时, 表单可以正确地显示出错信息了, 但是用户之前输入的数据都没有了. 你应该将这些数据重新放入表单中, 以便更好地提高用户体
验.
### Repopulating the Form重新填充表单数据
如例10-20所示, 当一个错误通过forward()方法处理后, 原来的请求数据仍是可以读取的, 而且用户输入的数据保存在请求参数中。 所以
你可以通过将缺省值加入到每个域中去来重新为表单填充数据, 见例10-25.
例10-25 验证失败后, 用缺省值为表单重新填充数据, 文件路径为`templates/indexSuccess.php`
[php]
<?php use_helper('Validation') ?>
<?php echo form_tag('contact/send') ?>
<?php echo form_error('name') ?><br />
Name: <?php echo input_tag('name', $sf_params->get('name')) ?
><br />
<?php echo form_error('email') ?><br />
Email: <?php echo input_tag('email', $sf_params-
>get('email')) ?><br />
<?php echo form_error('age') ?><br />
Age: <?php echo input_tag('age', $sf_params->get('age')) ?
><br />
<?php echo form_error('message') ?><br />
Message: <?php echo textarea_tag('message', $sf_params-
>get('message')) ?><br />
<?php echo submit_tag() ?>
</form>
当然写这些代码又是件很麻烦的事,所以symfony再次提供了一种方法让你不用改变元素的缺省值, 就可以重新为表单的所有域填充数据,这个方法就是
直接在YAML验证文件中设置表单的fillin属性,参见例10-26.
例10-26 激活fillin属性, 以便在验证失败时为表单重新填充数据, 文件路径为`validate/send.yml`
fillin:
enabled: true # Enable the form repopulation
param:
name: test # Form name, not needed if there is only one form
in the page
skip_fields: [email] # Do not repopulate these fields
exclude_types: [hidden, password] # Do not repopulate these
field types
check_types: [text, checkbox, radio, password, hidden] # Do
repopulate these
自动重新填充适用与文本框, 复选框, 单选按钮, 文本域和列表部件(包括简单列表和多重列表), 但不适用于密码和隐藏标域, 也不适用于文件标
记。
>**NOTE**
>The `fillin` feature works by parsing the response content in
XML just before sending it to the user. If the response is not a valid XHTML document, `fillin` might not work. `fillin`属性是通过编译将要发送给用户的XML文件中的响应内容,才能工作的,所以如果响应不是一个有效的XHTML文档, `fillin`就不能工作。
你有可能想在写入表单输入域之前对用户输入的值进行转换。只要你在converters属性下定义了可以用函数调用的转换,包括转义,URL重写,特殊
字符转换等,就可以将这些转换作用到表单的域上去。
例10-27 在fillin之前转换输入, 文件路径是`validate/send.yml`
fillin:
enabled: true
param:
name: test
converters: # Converters to apply
htmlentities: [first_name, comments]
htmlspecialchars: [comments]
### Standard Symfony Validators 标准symfony 验证器
symfony为你的表单提供了下列标准的验证器
* `sfStringValidator`
* `sfNumberValidator`
* `sfEmailValidator`
* `sfUrlValidator`
* `sfRegexValidator`
* `sfCompareValidator`
* `sfPropelUniqueValidator`
* `sfFileValidator`
* `sfCallbackValidator`
每个标准验证器都有一组缺省的参数和错误信息, 这些值和信息可以轻松地通过initialize()验证器方法或YAML文件来重新设置。 下面几节
内容将描述这些验证器并给出使用的例子。
#### String Validator字符串验证器
sfStringValidator 让你可以对参数进行与字符串有关的验证。
sfStringValidator:
values: [foo, bar]
values_error: The only accepted values are foo and bar
insensitive: false # If true, comparison with values is case
insensitive
min: 2
min_error: Please enter at least 2 characters
max: 100
max_error: Please enter less than 100 characters
#### Number Validator数字验证器
如果参数是一个数字, 你可以用sfNumberValidator来验证数的大小。
sfNumberValidator:
nan_error: Please enter an integer
min: 0
min_error: The value must be more than zero
max: 100
max_error: The value must be less than 100
#### E-Mail Validator E-Mail验证器
sfEmailValidator用于验证参数值是否符合电子邮件地址的格式标准。
sfEmailValidator:
strict: true
email_error: This email address is invalid
虽然RFC822定义了电子邮件的地址格式, 但通常认可的电子邮件地址格式要比这个标准更加严格。 比如, RFC认为me@localhost是有
效的电子邮件地址, 而你可能不能接受。 如果你将strict参数设定为true(这是缺省值), 那么只有符合
n...@domain.extension格式的电子邮件地址才能通过验证; 如果将该参数设定为false, 则采用RFC的规则。
#### URL Validator URL验证器
sfUrlValidator用于验证一个域的值是否是一个有效的URL.
sfUrlValidator:
url_error: This URL is invalid
#### Regular Expression Validator正则表达式验证器
sfRegexValidator让你验证一个值是否与一个Perl兼容正则表达式模式相匹配。
sfRegexValidator:
match: No
match_error: Posts containing more than one URL are considered
as spam
pattern: /http.*http/si
其中的match参数为Yes时, 表明请求参数与模式匹配为有效; 如果为No, 则表明请求参数与模式匹配为无效。
#### Compare Validator 比较验证器
sfCompareValidator用于检测两个不同的请求参数是否相同。 这对于密码检测非常有用。
fields:
password1:
required:
msg: Please enter a password
password2:
required:
msg: Please retype the password
sfCompareValidator:
check: password1
compare_error: The two passwords do not match
check参数是一个域的名字, 当前域必须要与该域匹配才有效。
#### Propel Unique Validator 唯一性验证器
sfPropelUniqueValidator检测一个请求参数的值是否在数据库中已经存在。这对于唯一索引非常有用.
fields:
nickname:
sfPropelUniqueValidator:
class: User
column: login
unique_error: This login already exists. Please choose
another one.
本例中, 验证器将在数据库中查找类User的记录, 以确定是否有和域值相同的 login 列。
#### File Validator文件验证器
sfFileValidator 用于检测文件上传域的文件格式(一组mime类型)和大小。
fields:
image:
required:
msg: Please upload an image file
file: True
sfFileValidator:
mime_types:
- 'image/jpeg'
- 'image/png'
- 'image/x-png'
- 'image/pjpeg'
mime_types_error: Only PNG and JPEG images are allowed
max_size: 512000
max_size_error: Max size is 512Kb
注意file属性必须设置为True, 并且模式中的表单也必须是multipart的。
#### Callback Validator回调验证器
sfCallbackValidator 可以委派第三方可调用方法或函数进行验证, 该可调用方法或函数必须能返回true或false值。
fields:
account_number:
sfCallbackValidator:
callback: is_integer
invalid_error: Please enter a number.
credit_card_number:
sfCallbackValidator:
callback: [myTools, validateCreditCard]
invalid_error: Please enter a valid credit card number.
回调方法或函数的第一个参数是需要验证的值。 当你想要重用现成的方法或函数时非常有用, 可以避免重建一个完整的验证器类。
>**TIP**
>You can also write your own validators, as described in the "Creating a Custom Validator" section later in this chapter.你也可以编写你自己的验证器, 本章后面的"创建定制的验证器"将具体描述。
### Named Validators 命名验证器
如果你会重用一个验证器类以及有关设置, 你可以将它们打包成一个命名验证器。 在联系人表单的例子中, email域要用到和name域相同的
sfStringValidator参数, 这样你就可以创建一个myStringValidator命名验证器 ,以免将所有设置重复一遍。你可以在
validators头下增加一个myStringValidator标签, 并将class和param属性设置为你需要的参数值。 然后你就可以在
fields中象使用标准验证器一样使用该命名验证器。 参见例10-28
例10-28 在验证文件中重用命名验证器, 文件路径为 `validate/send.yml`
validators:
myStringValidator:
class: sfStringValidator
param:
min: 2
min_error: This field is too short (2 characters minimum)
max: 100
max_error: This field is too long (100 characters maximum)
fields:
name:
required:
msg: The name field cannot be left blank
myStringValidator:
email:
required:
msg: The email field cannot be left blank
myStringValidator:
sfEmailValidator:
email_error: This email address is invalid
### Restricting the Validation to a Method重新指定验证方法
缺省情况下, 只要一个动作伴随着POST方式被调用, 则验证文件中设定的验证器都会执行。 你可以通过为methods属性制定其他的值, 而在全
局范围或针对每个域重新设置验证的方法,参见例10-29
例10-29 在`validate/send.yml`中定义何时测试一个域。
methods: [post] # This is the default setting
fields:
name:
required:
msg: The name field cannot be left blank
myStringValidator:
email:
methods: [post, get] # Overrides the global methods
settings
required:
msg: The email field cannot be left blank
myStringValidator:
sfEmailValidator:
email_error: This email address is invalid
### What Does a Validation File Look Like?验证文件全貌
到现在为止, 你还是只见到了验证文件的若干片段。 等你将这些全部合到一起, 验证规则就用YAML进行了清晰的描述。 例10-30是联系人表单的
完整验证文件, 它综合了前面定义的所有规则。
例10-30 完整的验证文件示例。
fillin:
enabled: true
validators:
myStringValidator:
class: sfStringValidator
param:
min: 2
min_error: This field is too short (2 characters minimum)
max: 100
max_error: This field is too long (100 characters maximum)
fields:
name:
required:
msg: The name field cannot be left blank
myStringValidator:
email:
required:
msg: The email field cannot be left blank
myStringValidator:
sfEmailValidator:
email_error: This email address is invalid
age:
sfNumberValidator
nan_error: Please enter an integer
min: 0
min_error: "You're not even born. How do you want to send
a message?"
max: 120
max_error: "Hey, grandma, aren't you too old to surf on
the Internet?"
message:
required:
msg: The message field cannot be left blank
Complex Validation复杂的验证
----------------------------
验证文件能满足大多数要求, 但是当验证非常复杂时, 它就不能满足需要了。 在这种情况下, 你仍就可以在动作中返回validateXXX()方
法, 或者在下面叙述的方法中找到解决方案。
### Creating a Custom Validator创建一个定制的验证器
每个验证器都是一个继承了sfValidator 类的子类。 如果symfony自带的验证器类不符合你的需要, 你可以在任何一个可以自动加载的
lib/目录中创建一个新的验证器类。语法很简单: 执行验证器就是执行类的execute()方法。 你也可以在initialize()方法中定义
缺省设置。
execute()方法对第一个参数的值进行验证, 以第二个参数的值作为出错信息。两个参数都以指针方式传送, 所以你可以在方法内部修改出错信息的
内容。
initialize()方法以上下文和YAML文件中的参数数组为参数。 定义initialize()时, 必须先调用父类sfValidator
的initialize()方法, 然后才能设置缺省值。
每个验证器都有一个参数容器, 它可以用$this->getParameterHolder()存取。
例如, 如果你想创建一个sfSpamValidator来检测一个字符串是否是一个垃圾信息串, 可以如例10-31那样在
sfSpamValidator.class.php中加入如下代码, 它可以检测出$values中是否包括超过由max_url属性定义的次数的的
字符串`http`。
例10-31 创建一个定制的验证器`lib/sfSpamValidator.class.php`
[php]
class sfSpamValidator extends sfValidator
{
public function execute (&$value, &$error)
{
// For max_url=2, the regexp is /http.*http/is
$re = '/'.implode('.*', array_fill(0, $this-
>getParameter('max_url') + 1, 'http')).'/is';
if (preg_match($re, $value))
{
$error = $this->getParameter('spam_error');
return false;
}
return true;
}
public function initialize ($context, $parameters = null)
{
// Initialize parent
parent::initialize($context);
// Set default parameters value
$this->setParameter('max_url', 2);
$this->setParameter('spam_error', 'This is spam');
// Set parameters
$this->getParameterHolder()->add($parameters);
return true;
}
}
一旦在可自动加载的目录中加入验证器(需清楚缓存), 你就可以在你的验证文件中使用它, 见例10-32.
例10-32 在validate/send.yml中使用定制的验证器
fields:
message:
required:
msg: The message field cannot be left blank
sfSpamValidator:
max_url: 3
spam_error: Leave this site immediately, you filthy
spammer!
### Using Array Syntax for Form Fields在表单域中使用数组名
在PHP中,你可以将数组用于表单域。 当你编写你自己的表单或用由Propel后台管理模块生成的表单(参见第14章)时, 你可以看到象例10-
33中显示的代码。
例10-33 使用数组名的表单
[php]
<label for="story[title]">Title:</label>
<input type="text" name="story[title]" id="story[title]"
value="default value"
size="45" />
在验证文件中使用一个带有方括号的输入名会导致编译错误。 解决的方法就是在fields段中用花括号`{}`去替代方括号`[]`, symfony
会处理传送到验证器方法的名字转换。参见例10-34.
例10-34 使用数组名的表单的验证文件
fields:
story{title}:
required: Yes
### Executing a Validator on an Empty Field验证空域
你可能需要验证一个并不一定要有值的域是否有一个空值。比如说,在一个表单中有一个密码域, 用户可以重设密码,这时,用户还必须输入一个确认密码;用
户也可以不改变密码。这个例子中就需要验证空域。 参见例10-35.
例10-35 具有两个密码域的表单的验证文件
fields:
password1:
password2:
sfCompareValidator:
check: password1
compare_error: The two passwords do not match
该验证过程按如下方式处理:
* 如果password1和password2都为空值:
* `required`测试通过
* 不执行验证器
* 表单有效
* 如果password2为空, 而password1不为空:
* `required`测试通过
* 不执行验证器
* 表单有效
你可能希望在password1为空时能执行你的password2验证器。 幸运的是, 利用group参数, symfony验证器可以处理这种情
形。 当一个域在一个组中时,如果这个域不为空或同一个组中的任一个域不为空, 这个域的验证器就会执行。
所以, 如果你象例10-36那样改变你的配置, 验证过程就能正确执行。
例10-36 带有两个密码域的表单和一个组的的验证文件
fields:c
password1:
group: password_group
password2:
group: password_group
sfCompareValidator:
check: password1
compare_error: The two passwords do not match
现在验证器按下述方式执行:
* 如果password1和password2都为空
* `required`测试通过
* 验证器未执行
* 表单有效
* 如果password1为空而password2为foo
* `required`测试通过
* 因为password2不为空, 所以将执行验证器, 验证失败
* password2的验证将抛出一个出错信息。
* 如果password1为foo 而password2为空
* `required`测试通过
* 同理, 因为password1不为空, 所以同一个组中的password2将执行验证器, 且验证失败
* password2的验证将抛出一个出错信息。
* 如果password1和password2都为foo.
* `required`测试通过
* 因为password2不为空, 所以将执行验证器, 验证成功
* 表单有效
Summary小结
-------
利用标准的form 辅助函数及附带的灵活的选项, 编写form非常方便. 如果你要设计一个可以编辑对象属性的表单时, 对象form 辅助函数将
提供更大的帮助. 验证文件, 验证辅助函数和重填特性可以减少编写一个强壮且用户友好的服务器控件所需的工作量. 写一个定制的验证器或在动作类中创
建一个validateXXX()方法, 就可以满足最复杂的验证需求..
[
本帖最后由 gegeu 于 2007-11-11 14:01 编辑 ]