Documentação do Symfony - versão 3.1
Renderizada do repositório symfony-docs-pt-BR no Github
Neste artigo, você vai aprender como criar um formulário que incorpora uma coleção
de muitos outros formulários. Isto pode ser útil, por exemplo, se você tem uma classe
Task
onde você deseja editar/criar/remover muitos objetos Tag
relacionados a
Task, dentro do mesmo formulário.
Note
Neste artigo, é livremente assumido que você está usando o Doctrine para armazenar em seu banco de dados. Mas se você não está usando o Doctrine (por exemplo, Propel ou apenas uma conexão de banco de dados), tudo é muito semelhante. Há apenas algumas partes deste tutorial que realmente se preocupam com “persistência”.
Se você está usando o Doctrine, você vai precisar adicionar os metadados do Doctrine,
incluindo a definição de mapeamento da associação ManyToMany
na propriedade
tags
da Task.
Vamos começar: suponha que cada Task
pertence a vários objetos
Tags
. Comece criando uma classe simples Task
:
// src/Acme/TaskBundle/Entity/Task.php
namespace Acme\TaskBundle\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;
}
public function setTags(ArrayCollection $tags)
{
$this->tags = $tags;
}
}
Note
O ArrayCollection
é específico do Doctrine e é basicamente o
mesmo que usar um array
(mas deve ser um ArrayCollection
se
você está usando o Doctrine).
Agora, crie uma classe Tag
. Como você viu acima, uma Task
pode ter muitos objetos
Tag
:
// src/Acme/TaskBundle/Entity/Tag.php
namespace Acme\TaskBundle\Entity;
class Tag
{
public $name;
}
Tip
A propriedade name
é pública aqui, mas ela pode facilmente ser protegida
ou privada (então seriam necessários os métodos getName
e setName
).
Agora, vamos para os formulários. Crie uma classe de formulário para que um objeto Tag
possa ser modificado pelo usuário:
// src/Acme/TaskBundle/Form/Type/TagType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Tag',
));
}
public function getName()
{
return 'tag';
}
}
Com isso, você tem o suficiente para renderizar um formulário tag. Mas, uma vez que o objetivo
final é permitir que as tags de uma Task
sejam modificadas dentro do próprio formulário da
task, crie um formulário para a classe Task
.
Observe que você embutiu uma coleção de formulários TagType
usando o
tipo de campo collection:
// src/Acme/TaskBundle/Form/Type/TaskType.php
namespace Acme\TaskBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array('type' => new TagType()));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
));
}
public function getName()
{
return 'task';
}
}
Em seu controlador, você irá agora inicializar uma nova instância do TaskType
:
// src/Acme/TaskBundle/Controller/TaskController.php
namespace Acme\TaskBundle\Controller;
use Acme\TaskBundle\Entity\Task;
use Acme\TaskBundle\Entity\Tag;
use Acme\TaskBundle\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
$tag1 = new Tag();
$tag1->name = 'tag1';
$task->getTags()->add($tag1);
$tag2 = new Tag();
$tag2->name = 'tag2';
$task->getTags()->add($tag2);
// end dummy code
$form = $this->createForm(new TaskType(), $task);
// process the form on POST
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
// ... maybe do some form processing, like saving the Task and Tag objects
}
}
return $this->render('AcmeTaskBundle:Task:new.html.twig', array(
'form' => $form->createView(),
));
}
}
O template correspondente agora é capaz de renderizar tanto o campo description
para
o formulário da task, quanto todos os formulários TagType
para quaisquer tags
que já estão relacionadas com esta Task
. No controlador acima, foi adicionado
algum código fictício para que você possa ver isso em ação (uma vez que uma Task
não tem
nenhuma tag quando ela é criada pela primeira vez).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | {# src/Acme/TaskBundle/Resources/views/Task/new.html.twig #}
{# ... #}
<form action="..." method="POST" {{ form_enctype(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 #}
{% for tag in form.tags %}
<li>{{ form_row(tag.name) }}</li>
{% endfor %}
</ul>
{{ form_rest(form) }}
{# ... #}
</form>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <!-- src/Acme/TaskBundle/Resources/views/Task/new.html.php -->
<!-- ... -->
<form action="..." method="POST" ...>
<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']->rest($form) ?>
</form>
<!-- ... -->
|
Quando o usuário submeter o formulário, os dados submetidos para os campos Tags
são usados para construir um ArrayCollection de objetos Tag
, o qual é então
definido no campo tag
da instância Task
.
A coleção Tags
é acessível naturalmente via $task->getTags()
e pode ser persistida no banco de dados ou utilizada da forma que você precisar.
Até agora, isso funciona muito bem, mas não permite que você adicione dinamicamente novas tags ou exclua as tags existentes. Então, enquanto a edição de tags existentes irá funcionar perfeitamente, o usuário não pode, ainda, adicionar quaisquer tags novas.
Caution
Neste artigo, você embutiu apenas uma coleção, mas você não está limitado
a apenas isto. Você também pode incorporar coleção aninhada com a quantidade de níveis abaixo
que desejar. Mas, se você usar o Xdebug em sua configuração de desenvolvimento, você pode receber
erro Maximum function nesting level of '100' reached, aborting!
.
Isto ocorre devido a configuração do PHP xdebug.max_nesting_level
, que tem como padrão
100
.
Esta diretiva limita recursão para 100 chamadas, o que pode não ser o suficiente para
renderizar o formulário no template se você renderizar todo o formulário de
uma vez (por exemplo, usando form_widget(form)
). Para corrigir isso, você pode definir
esta diretiva para um valor maior (através do arquivo ini do PHP ou via ini_set
,
por exemplo em app/autoload.php
) ou renderizar cada campo do formulário manualmente
usando form_row
.
Permitir ao usuário adicionar dinamicamente novas tags significa que você vai precisar usar algum JavaScript. Anteriormente, você adicionou duas tags ao seu formulário no controlador. Agora, para permitir ao usuário adicionar a quantidade de formulários tag que precisar diretamente no navegador, vamos utilizar um pouco de JavaScript.
A primeira coisa que você precisa fazer é tornar a coleção de formulário ciente de que ela vai
receber um número desconhecido de tags. Até agora, você adicionou duas tags e o tipo formulário
espera receber exatamente duas, caso contrário, um erro será lançado:
Este formulário não deve conter campos extras
. Para tornar isto flexível,
adicione a opção allow_add
no seu campo de coleção:
// src/Acme/TaskBundle/Form/Type/TaskType.php
// ...
use Symfony\Component\Form\FormBuilderInterface;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array(
'type' => new TagType(),
'allow_add' => true,
'by_reference' => false,
));
}
Note que 'by_reference' => false
também foi adicionado. Normalmente, o framework de formulário
irá modificar as tags em um objeto Task sem realmente
nunca chamar setTags. Definindo by_reference
para false, o setTags será chamado. Você verá que isto será importante
mais tarde.
Além de dizer ao campo para aceitar qualquer número de objetos submetidos, o
allow_add
também disponibiliza para você uma variável “prototype”. Este “prototype”
é um “template” que contém todo o HTML para poder renderizar quaisquer
formulários “tag” novos. Para renderizá-lo, faça a seguinte alteração no seu template:
1 2 3 | <ul class="tags" data-prototype="{{ form_widget(form.tags.vars.prototype)|e }}">
...
</ul>
|
1 2 3 | <ul class="tags" data-prototype="<?php echo $view->escape($view['form']->row($form['tags']->getVar('prototype'))) ?>">
...
</ul>
|
Note
Se você renderizar todo o seu sub-formulário “tags” de uma vez (por exemplo form_row(form.tags)
),
então o prototype está automaticamente disponível na div
externa, no
atributo data-prototype
, semelhante ao que você vê acima.
Tip
O form.tags.vars.prototype
é um elemento de formulário com o aspecto semelhante
aos elementos individuais form_widget(tag)
dentro do seu laço for
.
Isso significa que você pode chamar form_widget
, form_row
ou form_label
nele. Você pode até mesmo optar por renderizar apenas um de seus campos (por exemplo, o
campo name
):
1 | {{ form_widget(form.tags.vars.prototype.name)|e }}
|
Na página renderizada, o resultado será algo parecido com o seguinte:
1 | <ul class="tags" data-prototype="<div><label class=" required">__name__</label><div id="task_tags___name__"><div><label for="task_tags___name___name" class=" required">Name</label><input type="text" id="task_tags___name___name" name="task[tags][__name__][name]" required="required" maxlength="255" /></div></div></div>">
|
O objetivo desta seção será usar JavaScript para ler este atributo e dinamicamente adicionar novos formulários tag quando o usuário clicar no link “Adicionar uma tag”. Para tornar as coisas simples, este exemplo usa jQuery e assume que você o incluiu em algum lugar na sua página.
Adicione uma tag script
em algum lugar na sua página para que você possa começar a escrever um pouco de JavaScript.
Primeiro, adicione um link no final da lista “tags” via JavaScript. Segundo, faça o bind do evento
“click” desse link para que você possa adicionar um novo formulário de tag (addTagForm
será exibido em seguida):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // Get the ul that holds the collection of tags
var collectionHolder = $('ul.tags');
// 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() {
// add the "add a tag" anchor and li to the tags 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)
collectionHolder.data('index', collectionHolder.find(':input').length);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTagForm(collectionHolder, $newLinkLi);
});
});
|
O trabalho da função addTagForm
será usar o atributo data-prototype
para adicionar dinamicamente um novo formulário quando é clicado neste link. O HTML data-prototype
contém o elemento de entrada text
com um nome de task[tags][__name__][name]
e com o id task_tags___name___name
. O nome __name__
é um pequeno “placeholder”,
que você vai substituir por um número único, incrementado (por exemplo: task[tags][3][name]
).
New in version 2.1: O placeholder foi alterado de $$name$$
para __name__
no Symfony 2.1
O código real necessário para fazer todo este trabalho pode variar um pouco, mas aqui está um exemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function addTagForm(collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
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 the current collection's length.
var newForm = prototype.replace(/__name__/g, collectionHolder.children().length);
// 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
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
|
Note
É melhor separar o seu javascript em arquivos JavaScript do que escrevê-lo dentro do HTML como foi feito aqui.
Agora, cada vez que um usuário clicar no link Adicionar uma tag
, um novo sub-formulário vai
aparecer na página. Quando o formulário é submetido, todos os novos formulários de tag serão convertidos
em novos objetos Tag
e adicionados à propriedade tags
do objeto Task
.
O passo seguinte é permitir a remoção de um item em particular na coleção. A solução é similar a que permite que as tags sejam adicionadas.
Comece adicionando a opção allow_delete
no tipo do formulário:
// src/Acme/TaskBundle/Form/Type/TaskType.php
// ...
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('description');
$builder->add('tags', 'collection', array(
'type' => new TagType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
));
}
A opção allow_delete
tem uma consequência: se um item de uma coleção
não for enviado na submissão, o dado relacionado é removido da coleção
no servidor. A solução é, portanto, remover o elemento de formulário do DOM.
Primeiro, adicione um link “excluir esta tag” para cada formulário de tag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | jQuery(document).ready(function() {
// add a delete link to all of the existing tag form li elements
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);
}
|
A função addTagFormDeleteLink
será parecida com a seguinte:
1 2 3 4 5 6 7 8 9 10 11 12 | 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
e.preventDefault();
// remove the li for the tag form
$tagFormLi.remove();
});
}
|
Quando um formulário de tag é removido do DOM e submetido, o objeto Tag
removido
não será incluído na coleção passada ao setTags
. Dependendo de
sua camada de persistência, isto pode ou não ser o suficiente para remover efetivamente
a relação entre os objetos Tag
e Task
.