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,  为了当你生成管理模块的时候有此功能可用。

WordPress 所驱动