2009-06-18

内嵌表单详解(How to Embed Forms in Symfony 1.2 Admin Generator 中文版)

类归于: symfony — 标签:, , maker @ 23:31

译者:Maker.Wang[at]gmail.com
原文:http://sandbox-ws.com/how-to-embed-forms-in-symfony-12-admin-generator

译者注: Embed form是一个被忽略了的却很实用的技术,可能是由于embed form是新功能的缘故,相关的文档非常少,特别是中文文档,很早以前就听说了embed form,但一直没有机会实际应用, 昨天项目的新需求让我又想起了embed form, 所以又翻出了以前看过的文档, 仔细复习一遍, 同时也翻译出来分享个各位战友们。这是小弟的第一篇译文,难免有错误和遗漏,希望大家多包涵,多提宝贵意见。 – maker 2009.6.19

第一部分. 一对一关联内嵌表单

Symfony 1.2 添加了很多令人兴奋的新特性,并已经成为了一个很好很强大的PHP开发框架。 其中的一个特性是将一个Form嵌入另一个Form中。那么这意味着什么呢?

第一个模型如下图所示:

company_contact 上图是一个公司和联系方式的一对一关系模型。

这是我们的 schema.yml:

  1. propel:
  2. _attributes:
  3. package: lib.model
  4. defaultIdMethod: native
  5. company:
  6. _attributes: { phpName: Company }
  7. id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
  8. name: { type: VARCHAR, size: ‘255′, required: true }
  9. contact_id: { type: INTEGER, size: ‘11′, required: false, foreignTable: contact, foreignReference: id, onDelete: SET NULL, onUpdate: RESTRICT }
  10. _indexes: { company_FI_1: [contact_id] }
  11. contact:
  12. _attributes: { phpName: Contact }
  13. id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
  14. first_name: { type: VARCHAR, size: ‘255′, required: true }
  15. last_name: { type: VARCHAR, size: ‘255′, required: true }
  16. company_id: { type: INTEGER, size: ‘11′, required: false, foreignTable: company, foreignReference: id, onDelete: SET NULL, onUpdate: RESTRICT }
  17. _indexes: { contact_FI_1: [company_id] }

要注意的是这里的两个外键并不是必须存在(required)的,但是必须有了这两个外键我们才能将Contact Form嵌入到Company Form。

现在我们已经有了Schema, 让我们生成sql语句,模型(models), 表单(forms), 过滤器(filters), 并且创建数据表。

  1. $ php symfony propel:build-sql
  2. $ php symfony propel:build-model
  3. $ php symfony propel:build-forms
  4. $ php symfony propel:build-filters
  5. $ php symfony propel:insert-sql –env=dev

译者注:上面这组命令我们也可以简单的使用一个命令来完成

  1. $ php symfony propel:build-all

现在我们使用symfony中的admin generator来生成一个模块, 来看看如何生成和操作他们之间的关联。

  1. $ php symfony propel:generate-admin backend Company

我们生成了如下图所示的模块:

company_1现在如果我们点击“New”链接我们将看到界面如下:

company_2

注意外键”contact_id”被admin generator处理成一个下拉列表,当然这不是我们想要的结果,我们需要在新建Company的同时创建Contact. 感谢symfony 1.2 支持表单内嵌, 这会使我们的工作变得非常简单。

首先打开 “CompanyForm.class.php” 并且输入代码如下:

  1. public function configure() {
  2. // get Contact model object
  3. $contact = $this->getObject()->getContact();
  4. // contact object is null
  5. if (is_null($contact)) {
  6. // create a new Contact object
  7. $contact = new Contact();
  8. // set the copmany of the newly created object to the current company
  9. $contact->setCompany($this->getObject());
  10. // set the contact of the current company
  11. $this->getObject()->setContact($contact);
  12. }
  13. // create a new contact form
  14. $contact_form = new ContactForm($contact);
  15. // embed the contact form in the current company form
  16. $this->embedForm(‘contact’, $contact_form);
  17. // remove the contact_id from the form
  18. unset($this['contact_id']);
  19. }

接下来,打开ContactForm.class.php 并输入代码如下:

  1. public function configure() {
  2. unset($this['company_id']);
  3. }

现在修改模型类Company.php,在删除Contact的同时删除关联的Company

  1. public function delete(PropelPDO $con = null) {
  2. $this->getContact()->delete($con);
  3. parent::delete($con);
  4. }

最后一点也是很重要的,编辑generator.yml如下(这一步不是必须得) :

  1. generator:
  2. class: sfPropelGenerator
  3. param:
  4. model_class:           Company
  5. theme:                 admin
  6. non_verbose_templates: true
  7. with_show:             false
  8. singular:              ~
  9. plural:                ~
  10. route_prefix:          company
  11. with_propel_route:     1
  12. config:
  13. actions: ~
  14. fields:
  15. contact_id: { label: Company Contact }
  16. list:
  17. display: [=name, contact]
  18. filter:  ~
  19. form:    ~
  20. edit:    ~
  21. new:     ~

现在进入新建”Company”页面你会看到如图所示:

company_3尝试添加,编辑和删除一些记录以便确认一切是正确的。

company_4

company_5

第二部分. 一对多关联的内嵌表单

在文章的第一部分我们讲解了如何实一对一关联的表单内嵌。但是我们经常需要在编辑父对象的时候编辑或者添加子对象, 这并不是我们要使用admin generator的原因,总之在这部分文章里我们将要讲解一对多关系的表单内嵌。

在开始之前,我们看到的是最终要实现的CategoryForm。

embed_2

本文需要的文件

Ok, 我们开始吧,考虑下面这个schema:

  1. category:
  2. _attributes: { phpName: Category }
  3. id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
  4. name: { type: VARCHAR, size: ‘255′, required: true }
  5. created_at: { type: TIMESTAMP, required: false }
  6. updated_at: { type: TIMESTAMP, required: false }
  7. subcategory:
  8. _attributes: { phpName: Subcategory }
  9. id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
  10. name: { type: VARCHAR, size: ‘255′, required: true }
  11. category_id: { type: INTEGER, size: ‘11′, required: true, foreignTable: category, foreignReference: id, onDelete: CASCADE, onUpdate: RESTRICT }
  12. created_at: { type: TIMESTAMP, required: false }
  13. updated_at: { type: TIMESTAMP, required: false }

为了实现想要的效果,我们需要需要采取以下步骤

  1. 修改CategoryForm去包含当前分类所有的子分类的内嵌表单。
  2. 修改Widget中的子分类的name
  3. 修改CategoryForm添加一个空白的创建新子分类的表单。
  4. 重写sfForm类的bind方法,如果name域是空白则跳过保存(saving)和验证(validating)新子分类的表单。
  5. 在SubcategoryForm去掉全部的域(fields)

为当前分类插入全部子分类的内嵌表单[第1,2,3步]

  1. // lib/forms/CategoryForm.class.php
  2. public function configure() {
  3. // remove timestamps
  4. unset($this['created_at'], $this['updated_at']);
  5. // embed forms only when editing
  6. if (!$this->isNew()) {
  7. // embed all subcategory forms
  8. foreach ($this->getObject()->getSubcategorys() as $subcategory) {
  9. // create a new subcategory form for the current subcategory model object
  10. $subcategory_form = new SubcategoryForm($subcategory);
  11. // embed the subcategory form in the main category form
  12. $this->embedForm(’subcategory’.$subcategory->getId(), $subcategory_form);
  13. // set a custom label for the embedded form
  14. $this->widgetSchema['subcategory'.$subcategory->getId()]->setLabel(‘Subcategory: ’.$subcategory->getName());
  15. // change the name widget to sfWidgetFormInputDelete
  16. $this->widgetSchema['subcategory'.$subcategory->getId()]['name'] = new sfWidgetFormInputDelete(array(
  17. ‘url’ => ‘category/deleteSubcategory’, // required
  18. ‘model_id’ => $subcategory->getId(), // required
  19. ‘confirm’ => ‘Sure???’, // optional
  20. ));
  21. }
  22. // create a new subcategory form for a new subcategory model object
  23. $subcategory_form = new SubcategoryForm();
  24. // embed the subcategory form in the main category form
  25. $this->embedForm(’subcategory’, $subcategory_form);
  26. // set a custom label for the embedded form
  27. $this->widgetSchema['subcategory']->setLabel(‘New Subcategory’);
  28. }
  29. }

重写bind方法

  1. public function bind(array $taintedValues = null, array $taintedFiles = null) {
  2. // remove the embedded new form if the name field was not provided
  3. if (is_null($taintedValues['subcategory']['name']) || strlen($taintedValues['subcategory']['name']) === 0 ) {
  4. unset($this->embeddedForms['subcategory'], $taintedValues['subcategory']);
  5. // pass the new form validations
  6. $this->validatorSchema['subcategory'] = new sfValidatorPass();
  7. else {
  8. // set the category of the new subcategory form object
  9. $this->embeddedForms['subcategory']->getObject()->setCategory($this->getObject());
  10. }
  11. // call parent bind method
  12. parent::bind($taintedValues, $taintedFiles);
  13. }

在SubcategoryForm中的域

  1. public function configure(){
  2. unset($this['created_at'], $this['updated_at'], $this['category_id']);
  3. }

现在我们在分类模块下创建deleteSubcategory动作

  1. // apps/backend/modules/category/actions/actions.class.php
  2. public function executeDeleteSubcategory(sfWebRequest $request) {
  3. $sub_category = SubcategoryPeer::retrieveByPk($request->getParameter(‘id’));
  4. $sub_category->delete();
  5. $this->redirect(‘@category_edit?id=’.$sub_category->getCategory()->getId());
  6. }

就是这样了,我希望你会喜欢这篇文章。

第三部分. 内嵌表单的本地化

在本系列文章的上一部分我们介绍了在父对象中编辑多个子对象的方法。 在本部分钟我们将给子对象添加本地化行为, 为了更清楚的知道我们要做的事先看一下最终的效果。

embed_i18n

在开始之前,如果你没准备好,我强烈建议你去阅读一下第1,2部分。 OK, 我们开始吧,我们将要修改以下文件:

  • CategoryForm.class.php 分类模型的表单
  • SubcategoryI18nForm.class.php 子分类模型的本地化模型的表单
  • Subcategory.class.php 子分类模型

本文的附件

Schema.yml

  1. category:
  2. _attributes: { phpName: Category }
  3. id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
  4. name: { type: VARCHAR, size: ‘255′, required: true }
  5. created_at: { type: TIMESTAMP, required: false }
  6. updated_at: { type: TIMESTAMP, required: false }
  7. subcategory:
  8. _attributes: { phpName: Subcategory, isI18N: true, i18nTable: subcategory_i18n }
  9. id:          { type: integer, required: true, primaryKey: true, autoincrement: true }
  10. category_id: { type: INTEGER, size: ‘11′, required: true, foreignTable: category, foreignReference: id, onDelete: CASCADE}
  11. created_at: { type: TIMESTAMP, required: false }
  12. updated_at: { type: TIMESTAMP, required: false }
  13. subcategory_i18n:
  14. _attributes: { phpName: SubcategoryI18n }
  15. id:          { type: integer, required: true, primaryKey: true, foreignTable: subcategory, foreignReference: id, onDelete: CASCADE }
  16. culture:     { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true }
  17. name:        { type: varchar, size: 50 }

SubcategoryI18nForm.class.php

  1. public function configure() {
  2. unset($this['culture'], $this['id']);
  3. }

Subcategory.class.php

  1. public function getI18nObject($culture = ‘en’) {
  2. $this->setCulture($culture);
  3. $i18ns = $this->getSubcategoryI18ns();
  4. if (isset($i18ns[0])) {
  5. return $i18ns[0];
  6. }
  7. return null;
  8. }

CategoryForm.class.php

  1. public function configure() {
  2. // remove timestamps
  3. unset($this['created_at'], $this['updated_at']);
  4. // embed forms only when editing
  5. if (!$this->isNew()) {
  6. $user_culture = sfContext::getInstance()->getUser()->getCulture();
  7. // embed all subcategory forms
  8. foreach ($this->getObject()->getSubcategorys() as $subcategory) {
  9. // get the subcategory_i18n model object relative to the current user culture
  10. $subcategory_i18n_object = $subcategory->getI18nObject($user_culture);
  11. // create a new subcategory_i18n form for the current subcategory model object
  12. $subcategory_i18n_form = new SubcategoryI18nForm($subcategory->getI18nObject(‘en’));
  13. // get widget schema of subcategory_i18n form
  14. $subcategory_i18n_form_widget_schema = $subcategory_i18n_form->getWidgetSchema();
  15. // set the input delete widget
  16. $subcategory_i18n_form_widget_schema['name'] = new sfWidgetFormInputDelete(array(
  17. ‘url’ => ‘category/deleteSubcategory’, // required
  18. ‘model_id’ => $subcategory->getId(), // required
  19. ‘confirm’ => ‘Sure???’, // optional
  20. ));
  21. // create a new subcategory form for the current subcategory model object
  22. $subcategory_form = new SubcategoryForm($subcategory);
  23. // embed the i18n form
  24. $subcategory_form->embedForm(’subcategory_i18n’.$subcategory_i18n_object->getId(), $subcategory_i18n_form);
  25. // subcategory form widget schema
  26. $subcategory_form_widget_schema = $subcategory_form->getWidgetSchema();
  27. // disable label
  28. $subcategory_form_widget_schema['subcategory_i18n'.$subcategory_i18n_object->getId()]->setLabel(false);
  29. // embed the subcategory form in the main category form
  30. $this->embedForm(’subcategory’.$subcategory->getId(), $subcategory_form);
  31. // set a custom label for the embedded form
  32. $this->widgetSchema['subcategory'.$subcategory->getId()]->setLabel(‘Subcategory: ’.$subcategory->getName());
  33. }
  34. // create a new subcategory form for a new subcategory model object
  35. $subcategory_form = new SubcategoryForm();
  36. // create a new subcategory form for a new subcategory_i18n model object
  37. $subcategory_i18n_form = new SubcategoryI18nForm();
  38. // embed the subcategory_i18n form in the parent subcategory form
  39. $subcategory_form->embedForm(’subcategory_i18n’, $subcategory_i18n_form);
  40. // subcategory form widget schema
  41. $subcategory_form_widget_schema = $subcategory_form->getWidgetSchema();
  42. // disable label
  43. $subcategory_form_widget_schema['subcategory_i18n']->setLabel(false);
  44. // embed the subcategory form in the main category form
  45. $this->embedForm(’subcategory’, $subcategory_form);
  46. // set a custom label for the embedded form
  47. $this->widgetSchema['subcategory']->setLabel(‘New Subcategory’);
  48. }
  49. }
  50. public function bind(array $taintedValues = null, array $taintedFiles = null) {
  51. if(!$this->isNew()) {
  52. $user_culture = sfContext::getInstance()->getUser()->getCulture();
  53. // remove the embedded new form if the name field was not provided
  54. if (is_null($taintedValues['subcategory']['subcategory_i18n']['name']) || strlen($taintedValues['subcategory']['subcategory_i18n']['name']) === 0 ) {
  55. unset($this->embeddedForms['subcategory'], $taintedValues['subcategory']);
  56. // pass the new form validations
  57. $this->validatorSchema['subcategory'] = new sfValidatorPass();
  58. else {
  59. // set the category of the new subcategory form object
  60. $this->embeddedForms['subcategory']->getObject()->setCategory($this->getObject());
  61. // get subcategory embedded forms
  62. $subcategory_embedded_forms = $this->embeddedForms['subcategory']->getEmbeddedForms();
  63. // set subcategory parent of the subcategory_i18n model object
  64. $subcategory_embedded_forms['subcategory_i18n']->getObject()->setSubcategory($this->embeddedForms['subcategory']->getObject());
  65. // set the culture of the subcategory_i18n model object
  66. $subcategory_embedded_forms['subcategory_i18n']->getObject()->setCulture($user_culture);
  67. }
  68. }
  69. // call parent bind method
  70. parent::bind($taintedValues, $taintedFiles);
  71. }

正如你所看到的,这是功能需要你复制和粘贴大量的代码,这就是为什么我在使用扩展的Admin Generator,  为了当你生成管理模块的时候有此功能可用。

2009-04-15

CSRF使用注意事项和form的标准用法

类归于: symfony — 标签:, maker @ 13:17

我在<开启symfony的CSRF保护>一文中简单的介绍了CSRF的概念和使用方法, 由于那篇文章是学习symfony的初期写的, 所以对form和csrf理解的还不够透彻, 通过之后的学习和研究又渐渐的了解了一些细节.

首先, 在form被正确的应用的情况下, csrf是完全可以被兼容的, 这里先简单讲一下自定义的form的注意事项.

在定义form的时候, 我们需要定义名字格式, symfony自动生成的form应该已经设定了格式,不再需要重新设置

class CityForm extends BaseCityForm{
...
function setup()
{
...
$this->widgetSchema->setNameFormat('city[%s]');
...
}
...
}

这样我们将form中所有元素的值都放到了一个Parameter的子数组中.接下来在取得表单值的时候需要使用如下方法.

$this->form->bind($request->getParameter($this->form->getName()));
if($this->form->isValid()){
...
}else {
...
}

这样避免了手动构造bind的参数,也避免了在bind的参数中出现多余字段的问题,而且还可以完全兼容csrf,这也正是官方生成的代码所使用的方法。

本文完。

2008-12-26

Symfony中自定义form中offsetUnset的应用

类归于: symfony — 标签:, zhuozi @ 17:42

使用symfony,我们通常会重新定义我们需要的form,由于symfony机制的问题,它会根据你数据库的设置/config/databases.yml,自动判断form表单提交的数据是否符合标准。

我们常常遇到数据标准的问题就是,不可为空的字段,你没有给其赋值。

解决这个问题的办法,是在自定义的from里面

$this->offsetUnset(’字段名’); //重新定义一下,就不会出现这个错误了。

有的人可能会问,你直接修改数据的结构不是也可以解决嘛,对,修改数据库结构可以很容易的解决,可是如果你要是好几个模块都共用同一个表的时候怎么办呢?

比如说我有个表city有以下字段id,name,parent_id,info,pic

一级城市必须填写所有信息,二级城市只需填写name,parent_id字段

2008-12-22

form使用一例

类归于: symfony — 标签:, maker @ 14:51

symfony1.2中加入了一个很重要的特性, 就是form, 使用form可以快速的配置表单的相关操作, 增删改, 包括验证.

当然, 你也可以不对form进行设置, 因为form的生成是自动的, 当初始化model的时候是会在lib/form/下建立相应的的form, 和model一样, form也有一个用户可以自定义的类和一个base类.

下面是针对module shop的一个例子

__ lib/form/ShopForm.class.php __

class ShopForm extends BaseShopForm
{
// 用来生成选项的widget
var $last = array('Wang', 'Luo', 'Chen', 'Li');
public function configure()
{
$this->setWidgets(array(
// input text widget
'first' => new sfWidgetFormInput(array(), array('class' => 'class_name' , 'id' => 'id_name', 'value' => '屁股')),
// select widget
'last' => new sfWidgetFormSelect(array('choices' => $this->last)),
));
// set a label
$this->widgetSchema->setLabel('first', 'Your first name');
// set labels for widgets
$this->widgetSchema->setLabels(array( 'last' => 'Your last name',));
// 设置生成代码的方式, 可以是表格或者列表(li)
//$this->widgetSchema->setFormFormatterName('list');
// 设定生成的name的格式name="name[name]"
$this->widgetSchema->setNameFormat('name[%s]');
// 设定表单项的提示信息
$this->widgetSchema->setHelps(array('first' => 'first name', 'last' => 'last name'));
// 添加验证规则
$this->setValidators(array(
// 最小长度
'first' => new sfValidatorString(array('min_length' => 5), array('min_length' => '屁股的名字太短了')),
'last' => new sfValidatorString(array()),
));

// setPostValidator 是一个全局验证规则,错误信息会出现在表单上方
// validatorCallback 是一个自定义验证的方法
$this->validatorSchema->setPostValidator(new sfValidatorCallback(array('callback' => array($this, 'vcallback')), array('invalid' => '%value%错误了"')));
}

// 自定义验证方法
// $v应该是验证实例
// $s是form传进来的值
// $a应该是附加参数
function vcallback(&$v, $s, $a)
{
if (3>2)
// 如果验证失败返回一个validatorError实例
// validatorError的第三个参数用来替换上面callback时候的%value%
throw new sfValidatorError($v, 'invalid', array('value' => '屁股'));
}
}

__ apps/frontend/modules/shop/actions/actions.class.php __

public function executeIndex(sfWebRequest $request)
{
$this->form = new ShopForm();
if($request->isMethod('post'))
{
$this->form->bind($request->getParameter('name'));
if($this->form->isValid()){
// 如果通过验证则将参数传递到另外一个action中
$this->redirect('shop/submit?'.http_build_query($this->form->getValues()));
}
}
}

__ apps/frontend/modules/shop/templates/indexSuccess.php __

<form action='<?php echo url_for('shop/index');?>' method='post'>
<table border='1'>
<?php echo $form;?>
<tr>
<td colspan='2'>
<input type='submit' />
</td>
</tr>
</table>
</form>

2008-12-16

Form表单的新建、修改

类归于: symfony — 标签:, zhuozi @ 16:43

有的时候数据库中的table,有N个字段,但是在新建、修改的时候我们只要求用户创建或者修改其中的几个字段。如果不设置generator.yml和form,系统默认会显示所有,更新所有。

generator.yml

edit:
display: [ name, romaji, color, ordernum, state]
new:
display: [ name, romaji, color, ordernum state]

根目录lib\form\对应的form文件

public function configure()
{
//设置字段hidden
$this->setWidget('ordernum', new sfWidgetFormInputHidden());
//设置字段默认值
$this->setDefault('ordernum', 1);
}
早前文章 »

WordPress 所驱动