支付宝扫一扫付款
微信扫一扫付款
(微信为保护隐私,不显示你的昵称)
在本文中,你将会学习如何创建一个内嵌多个其他表单集合的表单。这可能会很有用,例如,你有一个Task
任务类(的表单),你想在同一表单之中,编辑/创建/删除和这个Task对象相关联的多个 Tag
标签对象。
Note
在本文中,估且假设你用Doctrine作为数据库存储。但是,如果你用的不是Doctrine(如,Propel或仅仅用了一个数据库连接),其实都差不多。本教程只有极少部分是关乎“Persistence”持久化的。
如果你用了Doctrine,你需要添加Doctrine元数据(memadata,译注:即annotation),在Task(任务)对象的 Tag
(标签)上定义 ManyToMany
关联映射。
首先,假设每一个 Task
都有多个 Tag
对象。从创建简单的 Task
类开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
class Task
{
protected $description;
protected $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getTags()
{
return $this->tags;
}
} |
Note
ArrayCollection
是 Doctrine 特有的(字段类型),基本上等同于使用了一个 array
(但是若你使用 Doctrine 就必须是 ArrayCollection
)。
现在,创建一个 Tag
类。正如你在上面看到的,一个 Task
可以有很多 Tag
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // src/AppBundle/Entity/Tag.php
namespace AppBundle\Entity;
class Tag
{
private $name;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
} |
然后,创建一个表单类,以便用户能够修改 Tag
对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // src/AppBundle/Form/Type/TagType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Tag;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Tag::class,
));
}
} |
有了这个,就足够该表单自行输出tag字段了。但由于最终目标是要让 Task
的标签(属性),可以在 任务表单 自身中被修改,所以还要为 Task
类创建一个表单。
注意,你使用了 collectionType 字段类型来嵌入 TagType
表单集合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;
use AppBundle\Entity\Task;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => Task::class,
));
}
} |
在控制器中,你要用 TaskType
创建一个新表单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | // src/AppBundle/Controller/TaskController.php
namespace AppBundle\Controller;
use AppBundle\Entity\Task;
use AppBundle\Entity\Tag;
use AppBundle\Form\Type\TaskType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class TaskController extends Controller
{
public function newAction(Request $request)
{
$task = new Task();
// dummy code - this is here just so that the Task has some tags
// otherwise, this isn't an interesting example
// 充数代码 - 仅在这里出现,以便Task能有一些tags
// 否则,这就不是一个有意义的例子了
$tag1 = new Tag();
$tag1->setName('tag1');
$task->getTags()->add($tag1);
$tag2 = new Tag();
$tag2->setName('tag2');
$task->getTags()->add($tag2);
// end dummy code / 充数代码结束
$form = $this->createForm(TaskType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// ... maybe do some form processing, like saving the Task and Tag objects
// ... 不妨进行一些表单处理,像是存住Task和Tag对象
}
return $this->render('task/new.html.twig', array(
'form' => $form->createView(),
));
}
} |
现在,相应的模板能够生成task表单的 description
字段以及 TagType
表单下的关联了 Task
的所有tag字段。在上面的控制器中我添加了一些充数代码,以便所以你能在action中看到这些(因为 Task
在最初创建时并没有 tag
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | {# app/Resources/views/task/new.html.twig #}
{# ... #}
{{ form_start(form) }}
{# render the task's only field: description #}
{{ form_row(form.description) }}
<h3>Tags</h3>
<ul class="tags">
{# iterate over each existing tag and render its only field: name #}
{# 对已有tag进行遍历,并输出唯一的字段,即: name #}
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
{{ form_end(form) }}
{# ... #} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <!-- src/AppBundle/Resources/views/Task/new.html.php -->
<!-- ... -->
<?php echo $view['form']->start($form) ?>
<!-- render the task's only field: description -->
<!-- 输出 task 的唯一字段: description -->
<?php echo $view['form']->row($form['description']) ?>
<h3>Tags</h3>
<ul class="tags">
<?php foreach($form['tags'] as $tag): ?>
<li><?php echo $view['form']->row($tag['name']) ?></li>
<?php endforeach ?>
</ul>
<?php echo $view['form']->end($form) ?>
<!-- ... --> |
在用户提交表单时,tags
字段所提交的数据将用于构造 Tag
对象的 ArrayCollection
,它会被用来设置 Task
实例的 tag
字段。
tags collection可以通过 $task->getTags()
原生访问到,可以被持久化到数据库中或随需使用。
到目前为止,程序运行良好,但却不允许你动态地添加一个新标签或者删除现有标签。所以,在编辑现有标签时是可用的,用户是不能添加任何新标签的。
Caution
本文中,你仅仅嵌入了一个集合,但你不会受限于此。你也可以嵌入你希望的任意多个嵌套集合(nested collection)。但如果你在开发过程中使用了Xdebug,你可能会遇到一个 Maximum function nesting level of '100' reached, aborting!
错误。这是由于PHP配置中的 xdebug.max_nesting_level
默认值是100所导致的。
如果你要一次输出所有表单(如form_widget(form)
),在模板生成表单时,这一指令的100次递归调用是不够用的。要解决此问题,你可以把该指令设置为一个较高的值(通过 php.ini
文件,或者通过 ini_set
,例如 app/autoload.php
),或者使用 form_row
来手动输出每一个表单字段。
允许用户动态地添加新的tag,意味着你需要使用一些JavaScript。之前你在控制器中把两个tag(标签)添加到了表单。现在要让用户在浏览器中直接添加任意多的标签到表单中。通过一些JavaScript来完成。
你要做的第一件事就是要让你的表单集合知道它将收到不明数量的标签。目前你已经添加了两个tag,而表单类型的预期就是两个,否则会抛出一个错误:This form should not contain extra fields
。为了灵活起见,添加 allow_add
选项到你的collection field(集合字段):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // src/AppBundle/Form/Type/TaskType.php
// ...
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class,
'allow_add' => true,
));
} |
除了要告诉这个字段要接收任意数量的对象之外,allow_add
也令得一个“prototype”变量能够为你所用。这个“原型”是一个小“模板”(template),它包含了输出任意tag表单时所需的的全部html。要生成它,在模板中做出以下修改:
1 2 3 | <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
...
</ul> |
1 2 3 4 5 | <ul class="tags" data-prototype="<?php
echo $view->escape($view['form']->row($form['tags']->vars['prototype']))
?>">
...
</ul> |
Note
如果你是一次性地输出“tag”子表单(如 form_row(form.tags)
),那么prototype会作为 data-prototype
属性,在外层的 div
上自动可用,就像上面你看到的那样。
Tip
form.tags.vars.prototype
是一个表单元素,可以当成是 for
循环里的个体 form_widget(tag)
元素。这意味着你可以对它调用form_widget
, form_row
和 form_label
。甚至可以选择只输出它的一个字段(如 name
字段):
1 | {{ form_widget(form.tags.vars.prototype.name)|e }} |
在输出页面,结果如下:
1 2 3 | <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e('html_attr') }}">
...
</ul> |
本文的目的就是使用JavaScript去读取这个属性,并且当用户点击 "Add a tag" 链接时,动态地加载新的tag表单。为了令事情简单,这个例子中使用了jQuery并且假设你页面中的某处已经包含了这个包。
在页面上添加 script
标签,你就可以开始编写一些JavaScript了。
首先,在“tag”列表的底部用JavaScript添加一个链接。然后,绑定该链接的onClick事件,这样你就能添加一个新的tag表单了(addTagForm()
方法会在下一段代码展示)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | var $collectionHolder;
// setup an "add a tag" link / 设置一个“添加一个标签”链接
var $addTagLink = $('<a href="#" class="add_tag_link">Add a tag</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
// 得到持有标签集合的ul
$collectionHolder = $('ul.tags');
// add the "add a tag" anchor and li to the tags ul
// 添加 "添加一个标签" 锚记到标签的ul元素中
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
// 计算我们当前持有的表单input数量(如2),并在插入新元素(如2)时把它作为新的索引
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
// 防止链接在URL中出现一个 "#"
e.preventDefault();
// add a new tag form (see next code block)
// 添加一个新的tag表单(参考后面的码段)
addTagForm($collectionHolder, $newLinkLi);
});
}); |
addTagForm()
函数的工作是,当这个链接被点击的时候,使用 data-prototype
属性来动态地添加一个新的表单。data-prototype
HTML码段中包含了标签的 text
input文本输入框元素,其name名为 task[tags][__name__][name]
,其id名为 task_tags___name___name
。__name__
是一个小“占位符”,你可以用一个唯一的、递增的数字来替换它(如 task[tags][3][name]
)。
令程序运行所需的代码可能会相当的多样化,好在这儿有一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | function addTagForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
// 获取前文解释过的data-prototype
var prototype = $collectionHolder.data('prototype');
// get the new index
// 获取新的索引
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
// 在prototype的HTML中替换 '__name__',令其成为一个基于“我们持有多少元素”的数字
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
// 下一个元素要增加一个索引
$collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
// 在页面中的 "Add a tag" 链接前面,以li来显示表单
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
} |
Note
最好把你的JavaScript代码分离到一个真正的JavaScript文件中去,比在这里直接写到HTML中要好。
现在,用户每次点击 Add a tag
链接时,一个新的子表单就会出现在页面上。当表单被提交时,所有的新标签表单都会被转换为一个新的 tag
对象,并被添加到 Task
对象的 tags
属性上。
Seealso
你可以在 JSFiddle 中找到这个例子的可操作版本。
Seealso
如果你想自定义 prototype 中的HTML代码,参考:如何定义Collection Prototype
为了更容易地处理这些新的标签,添加一个tag的“adder” 和 “remover” 方法到 Task
类中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;
// ...
class Task
{
// ...
public function addTag(Tag $tag)
{
$this->tags->add($tag);
}
public function removeTag(Tag $tag)
{
// ...
}
} |
接着,把一个 by_reference
选项添加到 tag
字段,并把它设置为 false
:
有了这两处更改,当表单被提交时,每一个新的 Tag
对象都是通过调用 addTag()
方法添加到 Task
类的。做出这个改变之前,它们是通过调用表单的 $task->getTags()->add($tag)
来从内部进行添加的。这样很好,但是强制使用“adder”方法会可以令处理新的 tag
对象更加容易(特别是当你用Doctrine时,你会在下面学习到!)。
Caution
你不得不同时创建 addTag()
和 removeTag()
方法,否则这个表单将继续使用 setTag
,即使你已经把 by_reference
设置为 false
。你将在下文中学习到更多关于 removeTag()
方法的内容。
下一步就是允许删除集合中的特定条目。解决方法类似于允许标签被添加进来。
从在form type表单类型中添加 allow_delete
选项开始:
现在,你需要向 Task
的 removeTag()
方法中加入一些代码:
1 2 3 4 5 6 7 8 9 10 11 12 | // src/AppBundle/Entity/Task.php
// ...
class Task
{
// ...
public function removeTag(Tag $tag)
{
$this->tags->removeElement($tag);
}
} |
allow_delete
选项有一个结果:如果集合中的某个元素没有能够随着提交而送出,那么在服务器端,该元素所对应的数据将从集合中被删除。解决办法是删除DOM中的表单元素。
首先,在每个tag表单中添加一个“delete this tag”链接(删除此标签):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
// 获取到持有“标签集合”的ul
$collectionHolder = $('ul.tags');
// add a delete link to all of the existing tag form li elements
// 在所有已有的tag表单的li中添加一个删除链接
$collectionHolder.find('li').each(function() {
addTagFormDeleteLink($(this));
});
// ... the rest of the block from above
// ...上述区块中的其余部分
});
function addTagForm() {
// ...
// add a delete link to the new form
// 添加一个“删除”链接到新的表单中
addTagFormDeleteLink($newFormLi);
} |
addTagFormDeleteLink()
函数可能是下面这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function addTagFormDeleteLink($tagFormLi) {
var $removeFormA = $('<a href="#">delete this tag</a>');
$tagFormLi.append($removeFormA);
$removeFormA.on('click', function(e) {
// prevent the link from creating a "#" on the URL
// 防止链接在URL中出现一个 "#"
e.preventDefault();
// remove the li for the tag form
//
$tagFormLi.remove();
});
} |
当一个tag表单从DOM中被移除并且提交时,这个移除的 Tag
对象将不包括在传递到 setTags
的集合之中。根据你的数据库层之不同,这可以,或者不太能够,切实地去除“被移除的 tag
标签和 Task
对象”之间的关系。
本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。