译者: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中。那么这意味着什么呢?
第一个模型如下图所示:
上图是一个公司和联系方式的一对一关系模型。
这是我们的 schema.yml:
- propel:
- _attributes:
- package: lib.model
- defaultIdMethod: native
- company:
- _attributes: { phpName: Company }
- id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
- name: { type: VARCHAR, size: ‘255′, required: true }
- contact_id: { type: INTEGER, size: ‘11′, required: false, foreignTable: contact, foreignReference: id, onDelete: SET NULL, onUpdate: RESTRICT }
- _indexes: { company_FI_1: [contact_id] }
- contact:
- _attributes: { phpName: Contact }
- id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
- first_name: { type: VARCHAR, size: ‘255′, required: true }
- last_name: { type: VARCHAR, size: ‘255′, required: true }
- company_id: { type: INTEGER, size: ‘11′, required: false, foreignTable: company, foreignReference: id, onDelete: SET NULL, onUpdate: RESTRICT }
- _indexes: { contact_FI_1: [company_id] }
要注意的是这里的两个外键并不是必须存在(required)的,但是必须有了这两个外键我们才能将Contact Form嵌入到Company Form。
现在我们已经有了Schema, 让我们生成sql语句,模型(models), 表单(forms), 过滤器(filters), 并且创建数据表。
- $ php symfony propel:build-sql
- $ php symfony propel:build-model
- $ php symfony propel:build-forms
- $ php symfony propel:build-filters
- $ php symfony propel:insert-sql –env=dev
译者注:上面这组命令我们也可以简单的使用一个命令来完成
- $ php symfony propel:build-all
现在我们使用symfony中的admin generator来生成一个模块, 来看看如何生成和操作他们之间的关联。
- $ php symfony propel:generate-admin backend Company
我们生成了如下图所示的模块:
现在如果我们点击“New”链接我们将看到界面如下:

注意外键”contact_id”被admin generator处理成一个下拉列表,当然这不是我们想要的结果,我们需要在新建Company的同时创建Contact. 感谢symfony 1.2 支持表单内嵌, 这会使我们的工作变得非常简单。
首先打开 “CompanyForm.class.php” 并且输入代码如下:
- public function configure() {
- // get Contact model object
- $contact = $this->getObject()->getContact();
- // contact object is null
- if (is_null($contact)) {
- // create a new Contact object
- $contact = new Contact();
- // set the copmany of the newly created object to the current company
- $contact->setCompany($this->getObject());
- // set the contact of the current company
- $this->getObject()->setContact($contact);
- }
- // create a new contact form
- $contact_form = new ContactForm($contact);
- // embed the contact form in the current company form
- $this->embedForm(‘contact’, $contact_form);
- // remove the contact_id from the form
- unset($this['contact_id']);
- }
接下来,打开ContactForm.class.php 并输入代码如下:
- public function configure() {
- unset($this['company_id']);
- }
现在修改模型类Company.php,在删除Contact的同时删除关联的Company
- public function delete(PropelPDO $con = null) {
- $this->getContact()->delete($con);
- parent::delete($con);
- }
最后一点也是很重要的,编辑generator.yml如下(这一步不是必须得) :
- generator:
- class: sfPropelGenerator
- param:
- model_class: Company
- theme: admin
- non_verbose_templates: true
- with_show: false
- singular: ~
- plural: ~
- route_prefix: company
- with_propel_route: 1
- config:
- actions: ~
- fields:
- contact_id: { label: Company Contact }
- list:
- display: [=name, contact]
- filter: ~
- form: ~
- edit: ~
- new: ~
现在进入新建”Company”页面你会看到如图所示:
尝试添加,编辑和删除一些记录以便确认一切是正确的。


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

本文需要的文件
Ok, 我们开始吧,考虑下面这个schema:
- category:
- _attributes: { phpName: Category }
- id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
- name: { type: VARCHAR, size: ‘255′, required: true }
- created_at: { type: TIMESTAMP, required: false }
- updated_at: { type: TIMESTAMP, required: false }
- subcategory:
- _attributes: { phpName: Subcategory }
- id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
- name: { type: VARCHAR, size: ‘255′, required: true }
- category_id: { type: INTEGER, size: ‘11′, required: true, foreignTable: category, foreignReference: id, onDelete: CASCADE, onUpdate: RESTRICT }
- created_at: { type: TIMESTAMP, required: false }
- updated_at: { type: TIMESTAMP, required: false }
为了实现想要的效果,我们需要需要采取以下步骤
- 修改CategoryForm去包含当前分类所有的子分类的内嵌表单。
- 修改Widget中的子分类的name
- 修改CategoryForm添加一个空白的创建新子分类的表单。
- 重写sfForm类的bind方法,如果name域是空白则跳过保存(saving)和验证(validating)新子分类的表单。
- 在SubcategoryForm去掉全部的域(fields)
为当前分类插入全部子分类的内嵌表单[第1,2,3步]
- // lib/forms/CategoryForm.class.php
- public function configure() {
- // remove timestamps
- unset($this['created_at'], $this['updated_at']);
- // embed forms only when editing
- if (!$this->isNew()) {
- // embed all subcategory forms
- foreach ($this->getObject()->getSubcategorys() as $subcategory) {
- // create a new subcategory form for the current subcategory model object
- $subcategory_form = new SubcategoryForm($subcategory);
- // embed the subcategory form in the main category form
- $this->embedForm(’subcategory’.$subcategory->getId(), $subcategory_form);
- // set a custom label for the embedded form
- $this->widgetSchema['subcategory'.$subcategory->getId()]->setLabel(‘Subcategory: ’.$subcategory->getName());
- // change the name widget to sfWidgetFormInputDelete
- $this->widgetSchema['subcategory'.$subcategory->getId()]['name'] = new sfWidgetFormInputDelete(array(
- ‘url’ => ‘category/deleteSubcategory’, // required
- ‘model_id’ => $subcategory->getId(), // required
- ‘confirm’ => ‘Sure???’, // optional
- ));
- }
- // create a new subcategory form for a new subcategory model object
- $subcategory_form = new SubcategoryForm();
- // embed the subcategory form in the main category form
- $this->embedForm(’subcategory’, $subcategory_form);
- // set a custom label for the embedded form
- $this->widgetSchema['subcategory']->setLabel(‘New Subcategory’);
- }
- }
重写bind方法
- public function bind(array $taintedValues = null, array $taintedFiles = null) {
- // remove the embedded new form if the name field was not provided
- if (is_null($taintedValues['subcategory']['name']) || strlen($taintedValues['subcategory']['name']) === 0 ) {
- unset($this->embeddedForms['subcategory'], $taintedValues['subcategory']);
- // pass the new form validations
- $this->validatorSchema['subcategory'] = new sfValidatorPass();
- } else {
- // set the category of the new subcategory form object
- $this->embeddedForms['subcategory']->getObject()->setCategory($this->getObject());
- }
- // call parent bind method
- parent::bind($taintedValues, $taintedFiles);
- }
在SubcategoryForm中的域
- public function configure(){
- unset($this['created_at'], $this['updated_at'], $this['category_id']);
- }
现在我们在分类模块下创建deleteSubcategory动作
- // apps/backend/modules/category/actions/actions.class.php
- public function executeDeleteSubcategory(sfWebRequest $request) {
- $sub_category = SubcategoryPeer::retrieveByPk($request->getParameter(‘id’));
- $sub_category->delete();
- $this->redirect(‘@category_edit?id=’.$sub_category->getCategory()->getId());
- }
就是这样了,我希望你会喜欢这篇文章。
第三部分. 内嵌表单的本地化
在本系列文章的上一部分我们介绍了在父对象中编辑多个子对象的方法。 在本部分钟我们将给子对象添加本地化行为, 为了更清楚的知道我们要做的事先看一下最终的效果。
在开始之前,如果你没准备好,我强烈建议你去阅读一下第1,2部分。 OK, 我们开始吧,我们将要修改以下文件:
- CategoryForm.class.php 分类模型的表单
- SubcategoryI18nForm.class.php 子分类模型的本地化模型的表单
- Subcategory.class.php 子分类模型
本文的附件
Schema.yml
- category:
- _attributes: { phpName: Category }
- id: { type: INTEGER, size: ‘11′, primaryKey: true, autoIncrement: true, required: true }
- name: { type: VARCHAR, size: ‘255′, required: true }
- created_at: { type: TIMESTAMP, required: false }
- updated_at: { type: TIMESTAMP, required: false }
- subcategory:
- _attributes: { phpName: Subcategory, isI18N: true, i18nTable: subcategory_i18n }
- id: { type: integer, required: true, primaryKey: true, autoincrement: true }
- category_id: { type: INTEGER, size: ‘11′, required: true, foreignTable: category, foreignReference: id, onDelete: CASCADE}
- created_at: { type: TIMESTAMP, required: false }
- updated_at: { type: TIMESTAMP, required: false }
- subcategory_i18n:
- _attributes: { phpName: SubcategoryI18n }
- id: { type: integer, required: true, primaryKey: true, foreignTable: subcategory, foreignReference: id, onDelete: CASCADE }
- culture: { isCulture: true, type: varchar, size: 7, required: true, primaryKey: true }
- name: { type: varchar, size: 50 }
SubcategoryI18nForm.class.php
- public function configure() {
- unset($this['culture'], $this['id']);
- }
Subcategory.class.php
- public function getI18nObject($culture = ‘en’) {
- $this->setCulture($culture);
- $i18ns = $this->getSubcategoryI18ns();
- if (isset($i18ns[0])) {
- return $i18ns[0];
- }
- return null;
- }
CategoryForm.class.php
- public function configure() {
- // remove timestamps
- unset($this['created_at'], $this['updated_at']);
- // embed forms only when editing
- if (!$this->isNew()) {
- $user_culture = sfContext::getInstance()->getUser()->getCulture();
- // embed all subcategory forms
- foreach ($this->getObject()->getSubcategorys() as $subcategory) {
- // get the subcategory_i18n model object relative to the current user culture
- $subcategory_i18n_object = $subcategory->getI18nObject($user_culture);
- // create a new subcategory_i18n form for the current subcategory model object
- $subcategory_i18n_form = new SubcategoryI18nForm($subcategory->getI18nObject(‘en’));
- // get widget schema of subcategory_i18n form
- $subcategory_i18n_form_widget_schema = $subcategory_i18n_form->getWidgetSchema();
- // set the input delete widget
- $subcategory_i18n_form_widget_schema['name'] = new sfWidgetFormInputDelete(array(
- ‘url’ => ‘category/deleteSubcategory’, // required
- ‘model_id’ => $subcategory->getId(), // required
- ‘confirm’ => ‘Sure???’, // optional
- ));
- // create a new subcategory form for the current subcategory model object
- $subcategory_form = new SubcategoryForm($subcategory);
- // embed the i18n form
- $subcategory_form->embedForm(’subcategory_i18n’.$subcategory_i18n_object->getId(), $subcategory_i18n_form);
- // subcategory form widget schema
- $subcategory_form_widget_schema = $subcategory_form->getWidgetSchema();
- // disable label
- $subcategory_form_widget_schema['subcategory_i18n'.$subcategory_i18n_object->getId()]->setLabel(false);
- // embed the subcategory form in the main category form
- $this->embedForm(’subcategory’.$subcategory->getId(), $subcategory_form);
- // set a custom label for the embedded form
- $this->widgetSchema['subcategory'.$subcategory->getId()]->setLabel(‘Subcategory: ’.$subcategory->getName());
- }
- // create a new subcategory form for a new subcategory model object
- $subcategory_form = new SubcategoryForm();
- // create a new subcategory form for a new subcategory_i18n model object
- $subcategory_i18n_form = new SubcategoryI18nForm();
- // embed the subcategory_i18n form in the parent subcategory form
- $subcategory_form->embedForm(’subcategory_i18n’, $subcategory_i18n_form);
- // subcategory form widget schema
- $subcategory_form_widget_schema = $subcategory_form->getWidgetSchema();
- // disable label
- $subcategory_form_widget_schema['subcategory_i18n']->setLabel(false);
- // embed the subcategory form in the main category form
- $this->embedForm(’subcategory’, $subcategory_form);
- // set a custom label for the embedded form
- $this->widgetSchema['subcategory']->setLabel(‘New Subcategory’);
- }
- }
- public function bind(array $taintedValues = null, array $taintedFiles = null) {
- if(!$this->isNew()) {
- $user_culture = sfContext::getInstance()->getUser()->getCulture();
- // remove the embedded new form if the name field was not provided
- if (is_null($taintedValues['subcategory']['subcategory_i18n']['name']) || strlen($taintedValues['subcategory']['subcategory_i18n']['name']) === 0 ) {
- unset($this->embeddedForms['subcategory'], $taintedValues['subcategory']);
- // pass the new form validations
- $this->validatorSchema['subcategory'] = new sfValidatorPass();
- } else {
- // set the category of the new subcategory form object
- $this->embeddedForms['subcategory']->getObject()->setCategory($this->getObject());
- // get subcategory embedded forms
- $subcategory_embedded_forms = $this->embeddedForms['subcategory']->getEmbeddedForms();
- // set subcategory parent of the subcategory_i18n model object
- $subcategory_embedded_forms['subcategory_i18n']->getObject()->setSubcategory($this->embeddedForms['subcategory']->getObject());
- // set the culture of the subcategory_i18n model object
- $subcategory_embedded_forms['subcategory_i18n']->getObject()->setCulture($user_culture);
- }
- }
- // call parent bind method
- parent::bind($taintedValues, $taintedFiles);
- }
正如你所看到的,这是功能需要你复制和粘贴大量的代码,这就是为什么我在使用扩展的Admin Generator, 为了当你生成管理模块的时候有此功能可用。
